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:
parent
ca633ae882
commit
080e1d98c6
22 changed files with 602 additions and 154 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)])
|
||||||
|
|
5
client/android/res/values-ru/strings.xml
Normal file
5
client/android/res/values-ru/strings.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="connecting">Подключение</string>
|
||||||
|
<string name="disconnecting">Отключение</string>
|
||||||
|
</resources>
|
5
client/android/res/values/strings.xml
Normal file
5
client/android/res/values/strings.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="connecting">Connecting</string>
|
||||||
|
<string name="disconnecting">Disconnecting</string>
|
||||||
|
</resources>
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
272
client/android/src/org/amnezia/vpn/AmneziaTileService.kt
Normal file
272
client/android/src/org/amnezia/vpn/AmneziaTileService.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
75
client/android/src/org/amnezia/vpn/VpnState.kt
Normal file
75
client/android/src/org/amnezia/vpn/VpnState.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue