Add Quick Settings tile (#660)

* Add Quick Settings tile

- Add multi-client support to AmneziaVpnService
- Make AmneziaActivity permanently connected to AmneziaVpnService while it is running
- Refactor processing of connection state changes on qt side
- Add VpnState DataStore
- Add check if AmneziaVpnService is running

* Add tile reset when the server is removed from the application
This commit is contained in:
albexk 2024-03-04 18:08:55 +03:00 committed by GitHub
parent ca633ae882
commit 080e1d98c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 602 additions and 154 deletions

View file

@ -95,7 +95,14 @@ void AmneziaApplication::init()
qFatal("Android logging initialization failed"); qFatal("Android logging initialization failed");
} }
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); 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, connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
[this](Vpn::ConnectionState state) { [this](Vpn::ConnectionState state) {

View file

@ -56,6 +56,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" /> android:value="-- %%INSERT_APP_LIB_NAME%% --" />
@ -146,6 +150,22 @@
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".AmneziaTileService"
android:process=":amneziaTileService"
android:icon="@drawable/ic_amnezia_round"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="org.amnezia.vpn.qtprovider" android:authorities="org.amnezia.vpn.qtprovider"

View file

@ -111,4 +111,5 @@ dependencies {
implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.coroutines)
implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit) implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
} }

View file

@ -6,6 +6,7 @@ androidx-activity = "1.8.1"
androidx-annotation = "1.7.0" androidx-annotation = "1.7.0"
androidx-camera = "1.3.0" androidx-camera = "1.3.0"
androidx-security-crypto = "1.1.0-alpha06" androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.0-beta01"
kotlinx-coroutines = "1.7.3" kotlinx-coroutines = "1.7.3"
google-mlkit = "17.2.0" google-mlkit = "17.2.0"
@ -18,6 +19,7 @@ androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.r
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" } androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" } androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" } google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }

View file

@ -2,9 +2,9 @@ package org.amnezia.vpn.protocol
// keep synchronized with client/platforms/android/android_controller.h ConnectionState // keep synchronized with client/platforms/android/android_controller.h ConnectionState
enum class ProtocolState { enum class ProtocolState {
DISCONNECTED,
CONNECTED, CONNECTED,
CONNECTING, CONNECTING,
DISCONNECTED,
DISCONNECTING, DISCONNECTING,
RECONNECTING, RECONNECTING,
UNKNOWN UNKNOWN

View file

@ -28,6 +28,10 @@ fun Bundle.putStatus(status: Status) {
putInt(STATE_KEY, status.state.ordinal) putInt(STATE_KEY, status.state.ordinal)
} }
fun Bundle.putStatus(state: ProtocolState) {
putInt(STATE_KEY, state.ordinal)
}
fun Bundle.getStatus(): Status = fun Bundle.getStatus(): Status =
Status.build { Status.build {
setState(ProtocolState.entries[getInt(STATE_KEY)]) setState(ProtocolState.entries[getInt(STATE_KEY)])

View file

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Подключение</string>
<string name="disconnecting">Отключение</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Connecting</string>
<string name="disconnecting">Disconnecting</string>
</resources>

View file

@ -26,9 +26,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.qt.QtAndroidController
@ -36,11 +34,11 @@ import org.amnezia.vpn.util.Log
import org.qtproject.qt.android.bindings.QtActivity import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity" private const val TAG = "AmneziaActivity"
const val ACTIVITY_MESSENGER_NAME = "Activity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1 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 BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() { class AmneziaActivity : QtActivity() {
@ -58,25 +56,17 @@ class AmneziaActivity : QtActivity() {
val event = msg.extractIpcMessage<ServiceEvent>() val event = msg.extractIpcMessage<ServiceEvent>()
Log.d(TAG, "Handle event: $event") Log.d(TAG, "Handle event: $event")
when (event) { when (event) {
ServiceEvent.CONNECTED -> { ServiceEvent.STATUS_CHANGED -> {
QtAndroidController.onVpnConnected() msg.data?.getStatus()?.let { (state) ->
} Log.d(TAG, "Handle protocol state: $state")
QtAndroidController.onVpnStateChanged(state.ordinal)
ServiceEvent.DISCONNECTED -> { }
QtAndroidController.onVpnDisconnected()
doUnbindService()
}
ServiceEvent.RECONNECTING -> {
QtAndroidController.onVpnReconnecting()
} }
ServiceEvent.STATUS -> { ServiceEvent.STATUS -> {
if (isWaitingStatus) { if (isWaitingStatus) {
isWaitingStatus = false isWaitingStatus = false
msg.data?.getStatus()?.let { (state) -> msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
QtAndroidController.onStatus(state.ordinal)
}
} }
} }
@ -87,7 +77,7 @@ class AmneziaActivity : QtActivity() {
} }
ServiceEvent.ERROR -> { ServiceEvent.ERROR -> {
msg.data?.getString(ERROR_MSG)?.let { error -> msg.data?.getString(MSG_ERROR)?.let { error ->
Log.e(TAG, "From VpnService: $error") Log.e(TAG, "From VpnService: $error")
} }
// todo: add error reporting to Qt // 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 // get a messenger from the service to send actions to the service
vpnServiceMessenger.set(Messenger(service)) vpnServiceMessenger.set(Messenger(service))
// send a messenger to the service to process service events // send a messenger to the service to process service events
vpnServiceMessenger.send { vpnServiceMessenger.send(
Action.REGISTER_CLIENT.packToMessage().apply { Action.REGISTER_CLIENT.packToMessage {
replyTo = activityMessenger putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
} },
} replyTo = activityMessenger
)
isServiceConnected = true isServiceConnected = true
if (isWaitingStatus) { if (isWaitingStatus) {
vpnServiceMessenger.send(Action.REQUEST_STATUS) vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
} }
} }
@ -126,6 +117,7 @@ class AmneziaActivity : QtActivity() {
vpnServiceMessenger.reset() vpnServiceMessenger.reset()
isWaitingStatus = true isWaitingStatus = true
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
doBindService()
} }
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
@ -148,8 +140,11 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Create Amnezia activity: $intent") Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
vpnServiceMessenger = IpcMessenger( vpnServiceMessenger = IpcMessenger(
onDeadObjectException = ::doUnbindService, "VpnService",
messengerName = "VpnService" onDeadObjectException = {
doUnbindService()
doBindService()
}
) )
intent?.let(::processIntent) intent?.let(::processIntent)
} }
@ -244,10 +239,9 @@ class AmneziaActivity : QtActivity() {
private fun doBindService() { private fun doBindService() {
Log.d(TAG, "Bind service") Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also { Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT) bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
} }
isInBoundState = true isInBoundState = true
handleBindTimeout()
} }
@MainThread @MainThread
@ -256,26 +250,14 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Unbind service") Log.d(TAG, "Unbind service")
isWaitingStatus = true isWaitingStatus = true
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
vpnServiceMessenger.reset()
isServiceConnected = false isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
isInBoundState = false isInBoundState = false
unbindService(serviceConnection) 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 * Methods of starting and stopping VpnService
*/ */
@ -312,7 +294,7 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Connect to VPN") Log.d(TAG, "Connect to VPN")
vpnServiceMessenger.send { vpnServiceMessenger.send {
Action.CONNECT.packToMessage { Action.CONNECT.packToMessage {
putString(VPN_CONFIG, vpnConfig) putString(MSG_VPN_CONFIG, vpnConfig)
} }
} }
} }
@ -320,7 +302,7 @@ class AmneziaActivity : QtActivity() {
private fun startVpnService(vpnConfig: String) { private fun startVpnService(vpnConfig: String) {
Log.d(TAG, "Start VPN service") Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply { Intent(this, AmneziaVpnService::class.java).apply {
putExtra(VPN_CONFIG, vpnConfig) putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also { }.also {
ContextCompat.startForegroundService(this, it) 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") @Suppress("unused")
fun saveFile(fileName: String, data: String) { fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName") Log.d(TAG, "Save file $fileName")
@ -438,7 +436,7 @@ class AmneziaActivity : QtActivity() {
Log.saveLogs = enabled Log.saveLogs = enabled
vpnServiceMessenger.send { vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage { Action.SET_SAVE_LOGS.packToMessage {
putBoolean(SAVE_LOGS, enabled) putBoolean(MSG_SAVE_LOGS, enabled)
} }
} }
} }

View file

@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
super.onCreate() super.onCreate()
Prefs.init(this) Prefs.init(this)
Log.init(this) Log.init(this)
VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application") Log.d(TAG, "Create Amnezia application")
createNotificationChannel() createNotificationChannel()
} }

View file

@ -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
}
}

View file

@ -1,7 +1,10 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
@ -16,6 +19,7 @@ import android.os.Process
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN 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.VpnException
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.awg.Awg import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.putStatistics
import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
@ -57,12 +59,16 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService" private const val TAG = "AmneziaVpnService"
const val VPN_CONFIG = "VPN_CONFIG" const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val ERROR_MSG = "ERROR_MSG" const val MSG_ERROR = "ERROR"
const val SAVE_LOGS = "SAVE_LOGS" const val MSG_SAVE_LOGS = "SAVE_LOGS"
const val MSG_CLIENT_NAME = "CLIENT_NAME"
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK" const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF" 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 NOTIFICATION_ID = 1337
private const val STATISTICS_SENDING_TIMEOUT = 1000L private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L private const val DISCONNECT_TIMEOUT = 5000L
@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
private var protocol: Protocol? = null private var protocol: Protocol? = null
private val protocolCache = mutableMapOf<String, Protocol>() private val protocolCache = mutableMapOf<String, Protocol>()
private var protocolState = MutableStateFlow(UNKNOWN) private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
private val isConnected private val isConnected
get() = protocolState.value == CONNECTED get() = protocolState.value == CONNECTED
@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null private var connectionJob: Job? = null
private var disconnectionJob: Job? = null private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null private var statisticsSendingJob: Job? = null
private lateinit var clientMessenger: IpcMessenger
private lateinit var networkState: NetworkState private lateinit var networkState: NetworkState
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
private val isActivityConnected
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Handle action: $action") Log.d(TAG, "Handle action: $action")
when (action) { when (action) {
Action.REGISTER_CLIENT -> { 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 -> { Action.CONNECT -> {
val vpnConfig = msg.data.getString(VPN_CONFIG) connect(msg.data.getString(MSG_VPN_CONFIG))
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
} }
Action.DISCONNECT -> { Action.DISCONNECT -> {
@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
} }
Action.REQUEST_STATUS -> { Action.REQUEST_STATUS -> {
clientMessenger.send { clientMessengers[msg.replyTo]?.let { clientMessenger ->
ServiceEvent.STATUS.packToMessage { clientMessenger.send {
putStatus(Status.build { ServiceEvent.STATUS.packToMessage {
setState(this@AmneziaVpnService.protocolState.value) putStatus(this@AmneziaVpnService.protocolState.value)
}) }
} }
} }
} }
Action.SET_SAVE_LOGS -> { 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") Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler) connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
clientMessenger = IpcMessenger(messengerName = "Client") loadServerData()
launchProtocolStateHandler() launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect) networkState = NetworkState(this, ::reconnect)
} }
@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
if (isAlwaysOnCompat) { if (isAlwaysOnCompat) {
Log.d(TAG, "Start service via Always-on") Log.d(TAG, "Start service via Always-on")
connect(Prefs.load(PREFS_CONFIG_KEY)) connect()
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) { } else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
Log.d(TAG, "Start service after permission check") Log.d(TAG, "Start service after permission check")
connect(Prefs.load(PREFS_CONFIG_KEY)) connect()
} else { } else {
Log.d(TAG, "Start service") Log.d(TAG, "Start service")
val vpnConfig = intent?.getStringExtra(VPN_CONFIG) connect(intent?.getStringExtra(MSG_VPN_CONFIG))
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
} }
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat) ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onBind by $intent") Log.d(TAG, "onBind by $intent")
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent) if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
isServiceBound = true isServiceBound = true
if (isConnected) launchSendingStatistics()
return vpnServiceMessenger.binder return vpnServiceMessenger.binder
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind by $intent") Log.d(TAG, "onUnbind by $intent")
if (intent?.action != SERVICE_INTERFACE) { if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = false if (clientMessengers.isEmpty()) {
stopSendingStatistics() isServiceBound = false
clientMessenger.reset() if (isUnknown || isDisconnected) stopService()
if (isUnknown || isDisconnected) stopService() }
} }
return true return true
} }
@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onRebind by $intent") Log.d(TAG, "onRebind by $intent")
if (intent?.action != SERVICE_INTERFACE) { if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = true isServiceBound = true
if (isConnected) launchSendingStatistics()
} }
super.onRebind(intent) super.onRebind(intent)
} }
@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
*/ */
private fun launchProtocolStateHandler() { private fun launchProtocolStateHandler() {
mainScope.launch { mainScope.launch {
protocolState.collect { protocolState -> // drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState") Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) { when (protocolState) {
CONNECTED -> { CONNECTED -> {
clientMessenger.send(ServiceEvent.CONNECTED)
networkState.bindNetworkListener() networkState.bindNetworkListener()
if (isServiceBound) launchSendingStatistics() if (isActivityConnected) launchSendingStatistics()
} }
DISCONNECTED -> { DISCONNECTED -> {
clientMessenger.send(ServiceEvent.DISCONNECTED)
networkState.unbindNetworkListener() networkState.unbindNetworkListener()
stopSendingStatistics() stopSendingStatistics()
if (!isServiceBound) stopService() if (!isServiceBound) stopService()
@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
} }
RECONNECTING -> { RECONNECTING -> {
clientMessenger.send(ServiceEvent.RECONNECTING)
stopSendingStatistics() stopSendingStatistics()
} }
CONNECTING, UNKNOWN -> {} CONNECTING, UNKNOWN -> {}
} }
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
putStatus(protocolState)
}
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
} }
} }
} }
@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
} }
@MainThread @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 if (isConnected || protocolState.value == CONNECTING) return
Log.d(TAG, "Start VPN connection") Log.d(TAG, "Start VPN connection")
@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
protocolState.value = CONNECTING protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig) val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) { if (config == null) {
onError("Invalid VPN config") onError("Invalid VPN config")
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
private fun onError(msg: String) { private fun onError(msg: String) {
Log.e(TAG, msg) Log.e(TAG, msg)
mainScope.launch { mainScope.launch {
clientMessenger.send { clientMessengers.send {
ServiceEvent.ERROR.packToMessage { ServiceEvent.ERROR.packToMessage {
putString(ERROR_MSG, msg) putString(MSG_ERROR, msg)
} }
} }
} }
} }
private fun parseConfigToJson(vpnConfig: String?): JSONObject? = private fun parseConfigToJson(vpnConfig: String): JSONObject? =
try { if (vpnConfig.isBlank()) {
vpnConfig?.let {
JSONObject(it)
}
} catch (e: JSONException) {
onError("Invalid VPN config json format: ${e.message}")
null 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<String>(PREFS_SERVER_NAME).ifBlank { null }
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
}
private fun checkPermission(): Boolean = private fun checkPermission(): Boolean =
if (prepare(applicationContext) != null) { if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply { Intent(this, VpnRequestActivity::class.java).apply {
@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
} else { } else {
true 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
}
}
} }

View file

@ -20,9 +20,7 @@ sealed interface IpcMessage {
} }
enum class ServiceEvent : IpcMessage { enum class ServiceEvent : IpcMessage {
CONNECTED, STATUS_CHANGED,
DISCONNECTED,
RECONNECTING,
STATUS, STATUS,
STATISTICS_UPDATE, STATISTICS_UPDATE,
ERROR ERROR
@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
enum class Action : IpcMessage { enum class Action : IpcMessage {
REGISTER_CLIENT, REGISTER_CLIENT,
UNREGISTER_CLIENT,
CONNECT, CONNECT,
DISCONNECT, DISCONNECT,
REQUEST_STATUS, REQUEST_STATUS,

View file

@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
private const val TAG = "IpcMessenger" private const val TAG = "IpcMessenger"
class IpcMessenger( class IpcMessenger(
messengerName: String? = null,
private val onDeadObjectException: () -> Unit = {}, private val onDeadObjectException: () -> Unit = {},
private val onRemoteException: () -> Unit = {}, private val onRemoteException: () -> Unit = {}
private val messengerName: String = "Unknown"
) { ) {
private var messenger: Messenger? = null 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) { fun set(messenger: Messenger) {
this.messenger = messenger this.messenger = messenger
@ -25,19 +35,29 @@ class IpcMessenger(
fun send(msg: () -> Message) = messenger?.sendMsg(msg()) fun send(msg: () -> Message) = messenger?.sendMsg(msg())
fun send(msg: Message, replyTo: Messenger) = messenger?.sendMsg(msg.apply { this.replyTo = replyTo })
fun <T> send(msg: T) fun <T> send(msg: T)
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage()) where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage())
fun <T> send(msg: T, replyTo: Messenger)
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage().apply { this.replyTo = replyTo })
private fun Messenger.sendMsg(msg: Message) { private fun Messenger.sendMsg(msg: Message) {
try { try {
send(msg) send(msg)
} catch (e: DeadObjectException) { } catch (e: DeadObjectException) {
Log.w(TAG, "$messengerName messenger is dead") Log.w(TAG, "$name messenger is dead")
messenger = null messenger = null
onDeadObjectException() onDeadObjectException()
} catch (e: RemoteException) { } 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() onRemoteException()
} }
} }
} }
fun Map<Messenger, IpcMessenger>.send(msg: () -> Message) = this.values.forEach { it.send(msg) }
fun <T> Map<Messenger, IpcMessenger>.send(msg: T)
where T : Enum<T>, T : IpcMessage = this.values.forEach { it.send(msg) }

View file

@ -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<VpnState> = 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<VpnState> {
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)
}
}
}
}

View file

@ -1,18 +1,23 @@
package org.amnezia.vpn.qt 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, * JNI functions of the AndroidController class from android_controller.cpp,
* called by events in the Android part of the client * called by events in the Android part of the client
*/ */
object QtAndroidController { object QtAndroidController {
fun onStatus(status: Status) = onStatus(status.state)
fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
external fun onStatus(stateCode: Int) external fun onStatus(stateCode: Int)
external fun onServiceDisconnected() external fun onServiceDisconnected()
external fun onServiceError() external fun onServiceError()
external fun onVpnPermissionRejected() external fun onVpnPermissionRejected()
external fun onVpnConnected() external fun onVpnStateChanged(stateCode: Int)
external fun onVpnDisconnected()
external fun onVpnReconnecting()
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String) external fun onFileOpened(uri: String)

View file

@ -56,26 +56,10 @@ AndroidController::AndroidController() : QObject()
Qt::QueuedConnection); Qt::QueuedConnection);
connect( connect(
this, &AndroidController::vpnConnected, this, this, &AndroidController::vpnStateChanged, this,
[this]() { [this](AndroidController::ConnectionState state) {
qDebug() << "Android event: VPN connected"; qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
emit connectionStateChanged(Vpn::ConnectionState::Connected); emit connectionStateChanged(convertState(state));
},
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);
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@ -106,9 +90,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)}, {"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)}, {"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)}, {"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onVpnConnected", "()V", reinterpret_cast<void *>(onVpnConnected)}, {"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
@ -158,6 +140,11 @@ void AndroidController::stop()
callActivityMethod("stop", "()V"); callActivityMethod("stop", "()V");
} }
void AndroidController::resetLastServer(int serverIndex)
{
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::saveFile(const QString &fileName, const QString &data) void AndroidController::saveFile(const QString &fileName, const QString &data)
{ {
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V", callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
@ -370,30 +357,14 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
} }
// static // static
void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz) void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{ {
Q_UNUSED(env); Q_UNUSED(env);
Q_UNUSED(thiz); Q_UNUSED(thiz);
emit AndroidController::instance()->vpnConnected(); auto state = ConnectionState(stateCode);
}
// static emit AndroidController::instance()->vpnStateChanged(state);
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();
} }
// static // static

View file

@ -20,9 +20,9 @@ public:
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState // keep synchronized with org.amnezia.vpn.protocol.ProtocolState
enum class ConnectionState enum class ConnectionState
{ {
DISCONNECTED,
CONNECTED, CONNECTED,
CONNECTING, CONNECTING,
DISCONNECTED,
DISCONNECTING, DISCONNECTING,
RECONNECTING, RECONNECTING,
UNKNOWN UNKNOWN
@ -30,6 +30,7 @@ public:
ErrorCode start(const QJsonObject &vpnConfig); ErrorCode start(const QJsonObject &vpnConfig);
void stop(); void stop();
void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec); void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data); void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter); QString openFile(const QString &filter);
@ -48,9 +49,7 @@ signals:
void serviceDisconnected(); void serviceDisconnected();
void serviceError(); void serviceError();
void vpnPermissionRejected(); void vpnPermissionRejected();
void vpnConnected(); void vpnStateChanged(ConnectionState state);
void vpnDisconnected();
void vpnReconnecting();
void statisticsUpdated(quint64 rxBytes, quint64 txBytes); void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri); void fileOpened(QString uri);
void configImported(QString config); void configImported(QString config);
@ -77,9 +76,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz); static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz); static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz); static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
static void onVpnConnected(JNIEnv *env, jobject thiz); static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);

View file

@ -20,6 +20,7 @@ namespace amnezia
constexpr char dns1[] = "dns1"; constexpr char dns1[] = "dns1";
constexpr char dns2[] = "dns2"; constexpr char dns2[] = "dns2";
constexpr char serverIndex[] = "serverIndex";
constexpr char description[] = "description"; constexpr char description[] = "description";
constexpr char name[] = "name"; constexpr char name[] = "name";
constexpr char cert[] = "cert"; constexpr char cert[] = "cert";

View file

@ -68,6 +68,7 @@ void Settings::removeServer(int index)
servers.removeAt(index); servers.removeAt(index);
setServersArray(servers); setServersArray(servers);
emit serverRemoved(index);
} }
bool Settings::editServer(int index, const QJsonObject &server) bool Settings::editServer(int index, const QJsonObject &server)
@ -338,6 +339,7 @@ QString Settings::secondaryDns() const
void Settings::clearSettings() void Settings::clearSettings()
{ {
m_settings.clearSettings(); m_settings.clearSettings();
emit settingsCleared();
} }
ServerCredentials Settings::defaultServerCredentials() const ServerCredentials Settings::defaultServerCredentials() const

View file

@ -191,6 +191,8 @@ public:
signals: signals:
void saveLogsChanged(bool enabled); void saveLogsChanged(bool enabled);
void serverRemoved(int serverIndex);
void settingsCleared();
private: private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;

View file

@ -270,6 +270,7 @@ QJsonObject VpnConnection::createVpnConfiguration(int serverIndex, const ServerC
ErrorCode *errorCode) ErrorCode *errorCode)
{ {
QJsonObject vpnConfiguration; QJsonObject vpnConfiguration;
vpnConfiguration[config_key::serverIndex] = serverIndex;
for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) { for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) {
auto s = m_settings->server(serverIndex); auto s = m_settings->server(serverIndex);
@ -471,10 +472,15 @@ void VpnConnection::disconnectFromVpn()
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
if (m_vpnProtocol && m_vpnProtocol.data()) { if (m_vpnProtocol && m_vpnProtocol.data()) {
connect(AndroidController::instance(), &AndroidController::vpnDisconnected, this, auto *const connection = new QMetaObject::Connection;
[this]() { *connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this,
onConnectionStateChanged(Vpn::ConnectionState::Disconnected); [this, connection](AndroidController::ConnectionState state) {
}, Qt::SingleShotConnection); if (state == AndroidController::ConnectionState::DISCONNECTED) {
onConnectionStateChanged(Vpn::ConnectionState::Disconnected);
disconnect(*connection);
delete connection;
}
});
m_vpnProtocol.data()->stop(); m_vpnProtocol.data()->stop();
} }
#endif #endif