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
|
@ -56,6 +56,10 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
|
||||
|
@ -146,6 +150,22 @@
|
|||
</intent-filter>
|
||||
</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
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.amnezia.vpn.qtprovider"
|
||||
|
|
|
@ -111,4 +111,5 @@ dependencies {
|
|||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.bundles.androidx.camera)
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ androidx-activity = "1.8.1"
|
|||
androidx-annotation = "1.7.0"
|
||||
androidx-camera = "1.3.0"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
androidx-datastore = "1.1.0-beta01"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
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-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-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
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" }
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ package org.amnezia.vpn.protocol
|
|||
|
||||
// keep synchronized with client/platforms/android/android_controller.h ConnectionState
|
||||
enum class ProtocolState {
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
DISCONNECTING,
|
||||
RECONNECTING,
|
||||
UNKNOWN
|
||||
|
|
|
@ -28,6 +28,10 @@ fun Bundle.putStatus(status: Status) {
|
|||
putInt(STATE_KEY, status.state.ordinal)
|
||||
}
|
||||
|
||||
fun Bundle.putStatus(state: ProtocolState) {
|
||||
putInt(STATE_KEY, state.ordinal)
|
||||
}
|
||||
|
||||
fun Bundle.getStatus(): Status =
|
||||
Status.build {
|
||||
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.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
|
@ -36,11 +34,11 @@ import org.amnezia.vpn.util.Log
|
|||
import org.qtproject.qt.android.bindings.QtActivity
|
||||
|
||||
private const val TAG = "AmneziaActivity"
|
||||
const val ACTIVITY_MESSENGER_NAME = "Activity"
|
||||
|
||||
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val BIND_SERVICE_TIMEOUT = 1000L
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
|
@ -58,25 +56,17 @@ class AmneziaActivity : QtActivity() {
|
|||
val event = msg.extractIpcMessage<ServiceEvent>()
|
||||
Log.d(TAG, "Handle event: $event")
|
||||
when (event) {
|
||||
ServiceEvent.CONNECTED -> {
|
||||
QtAndroidController.onVpnConnected()
|
||||
}
|
||||
|
||||
ServiceEvent.DISCONNECTED -> {
|
||||
QtAndroidController.onVpnDisconnected()
|
||||
doUnbindService()
|
||||
}
|
||||
|
||||
ServiceEvent.RECONNECTING -> {
|
||||
QtAndroidController.onVpnReconnecting()
|
||||
ServiceEvent.STATUS_CHANGED -> {
|
||||
msg.data?.getStatus()?.let { (state) ->
|
||||
Log.d(TAG, "Handle protocol state: $state")
|
||||
QtAndroidController.onVpnStateChanged(state.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
ServiceEvent.STATUS -> {
|
||||
if (isWaitingStatus) {
|
||||
isWaitingStatus = false
|
||||
msg.data?.getStatus()?.let { (state) ->
|
||||
QtAndroidController.onStatus(state.ordinal)
|
||||
}
|
||||
msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +77,7 @@ class AmneziaActivity : QtActivity() {
|
|||
}
|
||||
|
||||
ServiceEvent.ERROR -> {
|
||||
msg.data?.getString(ERROR_MSG)?.let { error ->
|
||||
msg.data?.getString(MSG_ERROR)?.let { error ->
|
||||
Log.e(TAG, "From VpnService: $error")
|
||||
}
|
||||
// todo: add error reporting to Qt
|
||||
|
@ -109,14 +99,15 @@ class AmneziaActivity : QtActivity() {
|
|||
// get a messenger from the service to send actions to the service
|
||||
vpnServiceMessenger.set(Messenger(service))
|
||||
// send a messenger to the service to process service events
|
||||
vpnServiceMessenger.send {
|
||||
Action.REGISTER_CLIENT.packToMessage().apply {
|
||||
replyTo = activityMessenger
|
||||
}
|
||||
}
|
||||
vpnServiceMessenger.send(
|
||||
Action.REGISTER_CLIENT.packToMessage {
|
||||
putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
|
||||
},
|
||||
replyTo = activityMessenger
|
||||
)
|
||||
isServiceConnected = true
|
||||
if (isWaitingStatus) {
|
||||
vpnServiceMessenger.send(Action.REQUEST_STATUS)
|
||||
vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,6 +117,7 @@ class AmneziaActivity : QtActivity() {
|
|||
vpnServiceMessenger.reset()
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
doBindService()
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
|
@ -148,8 +140,11 @@ class AmneziaActivity : QtActivity() {
|
|||
Log.d(TAG, "Create Amnezia activity: $intent")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
vpnServiceMessenger = IpcMessenger(
|
||||
onDeadObjectException = ::doUnbindService,
|
||||
messengerName = "VpnService"
|
||||
"VpnService",
|
||||
onDeadObjectException = {
|
||||
doUnbindService()
|
||||
doBindService()
|
||||
}
|
||||
)
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
|
@ -244,10 +239,9 @@ class AmneziaActivity : QtActivity() {
|
|||
private fun doBindService() {
|
||||
Log.d(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
|
||||
}
|
||||
isInBoundState = true
|
||||
handleBindTimeout()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
|
@ -256,26 +250,14 @@ class AmneziaActivity : QtActivity() {
|
|||
Log.d(TAG, "Unbind service")
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
vpnServiceMessenger.reset()
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
|
||||
vpnServiceMessenger.reset()
|
||||
isInBoundState = false
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBindTimeout() {
|
||||
mainScope.launch {
|
||||
if (isWaitingStatus) {
|
||||
delay(BIND_SERVICE_TIMEOUT)
|
||||
if (isWaitingStatus && !isServiceConnected) {
|
||||
Log.d(TAG, "Bind timeout, reset connection status")
|
||||
isWaitingStatus = false
|
||||
QtAndroidController.onStatus(ProtocolState.DISCONNECTED.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods of starting and stopping VpnService
|
||||
*/
|
||||
|
@ -312,7 +294,7 @@ class AmneziaActivity : QtActivity() {
|
|||
Log.d(TAG, "Connect to VPN")
|
||||
vpnServiceMessenger.send {
|
||||
Action.CONNECT.packToMessage {
|
||||
putString(VPN_CONFIG, vpnConfig)
|
||||
putString(MSG_VPN_CONFIG, vpnConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -320,7 +302,7 @@ class AmneziaActivity : QtActivity() {
|
|||
private fun startVpnService(vpnConfig: String) {
|
||||
Log.d(TAG, "Start VPN service")
|
||||
Intent(this, AmneziaVpnService::class.java).apply {
|
||||
putExtra(VPN_CONFIG, vpnConfig)
|
||||
putExtra(MSG_VPN_CONFIG, vpnConfig)
|
||||
}.also {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
}
|
||||
|
@ -369,6 +351,22 @@ class AmneziaActivity : QtActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun resetLastServer(index: Int) {
|
||||
Log.v(TAG, "Reset server: $index")
|
||||
mainScope.launch {
|
||||
VpnStateStore.store {
|
||||
if (index == -1 || it.serverIndex == index) {
|
||||
VpnState.defaultState
|
||||
} else if (it.serverIndex > index) {
|
||||
it.copy(serverIndex = it.serverIndex - 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveFile(fileName: String, data: String) {
|
||||
Log.d(TAG, "Save file $fileName")
|
||||
|
@ -438,7 +436,7 @@ class AmneziaActivity : QtActivity() {
|
|||
Log.saveLogs = enabled
|
||||
vpnServiceMessenger.send {
|
||||
Action.SET_SAVE_LOGS.packToMessage {
|
||||
putBoolean(SAVE_LOGS, enabled)
|
||||
putBoolean(MSG_SAVE_LOGS, enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
|
|||
super.onCreate()
|
||||
Prefs.init(this)
|
||||
Log.init(this)
|
||||
VpnStateStore.init(this)
|
||||
Log.d(TAG, "Create Amnezia application")
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
|
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
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
|
||||
|
@ -16,6 +19,7 @@ import android.os.Process
|
|||
import androidx.annotation.MainThread
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -39,14 +44,11 @@ import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
|||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.Status
|
||||
import org.amnezia.vpn.protocol.VpnException
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.protocol.awg.Awg
|
||||
import org.amnezia.vpn.protocol.cloak.Cloak
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.protocol.putStatistics
|
||||
import org.amnezia.vpn.protocol.putStatus
|
||||
import org.amnezia.vpn.protocol.wireguard.Wireguard
|
||||
import org.amnezia.vpn.util.Log
|
||||
|
@ -57,12 +59,16 @@ import org.json.JSONObject
|
|||
|
||||
private const val TAG = "AmneziaVpnService"
|
||||
|
||||
const val VPN_CONFIG = "VPN_CONFIG"
|
||||
const val ERROR_MSG = "ERROR_MSG"
|
||||
const val SAVE_LOGS = "SAVE_LOGS"
|
||||
const val MSG_VPN_CONFIG = "VPN_CONFIG"
|
||||
const val MSG_ERROR = "ERROR"
|
||||
const val MSG_SAVE_LOGS = "SAVE_LOGS"
|
||||
const val MSG_CLIENT_NAME = "CLIENT_NAME"
|
||||
|
||||
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
|
||||
private const val PREFS_CONFIG_KEY = "LAST_CONF"
|
||||
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
|
||||
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
|
||||
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
|
||||
private const val NOTIFICATION_ID = 1337
|
||||
private const val STATISTICS_SENDING_TIMEOUT = 1000L
|
||||
private const val DISCONNECT_TIMEOUT = 5000L
|
||||
|
@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
|
|||
private var protocol: Protocol? = null
|
||||
private val protocolCache = mutableMapOf<String, Protocol>()
|
||||
private var protocolState = MutableStateFlow(UNKNOWN)
|
||||
private var serverName: String? = null
|
||||
private var serverIndex: Int = -1
|
||||
|
||||
private val isConnected
|
||||
get() = protocolState.value == CONNECTED
|
||||
|
@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
|
|||
private var connectionJob: Job? = null
|
||||
private var disconnectionJob: Job? = null
|
||||
private var statisticsSendingJob: Job? = null
|
||||
private lateinit var clientMessenger: IpcMessenger
|
||||
private lateinit var networkState: NetworkState
|
||||
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
|
||||
|
||||
private val isActivityConnected
|
||||
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
|
||||
|
||||
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
|
||||
protocolState.value = DISCONNECTED
|
||||
|
@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
|
|||
Log.d(TAG, "Handle action: $action")
|
||||
when (action) {
|
||||
Action.REGISTER_CLIENT -> {
|
||||
clientMessenger.set(msg.replyTo)
|
||||
val clientName = msg.data.getString(MSG_CLIENT_NAME)
|
||||
val messenger = IpcMessenger(msg.replyTo, clientName)
|
||||
clientMessengers[msg.replyTo] = messenger
|
||||
Log.d(TAG, "Messenger client '$clientName' was registered")
|
||||
if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
|
||||
}
|
||||
|
||||
Action.UNREGISTER_CLIENT -> {
|
||||
clientMessengers.remove(msg.replyTo)?.let {
|
||||
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
|
||||
if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
Action.CONNECT -> {
|
||||
val vpnConfig = msg.data.getString(VPN_CONFIG)
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connect(vpnConfig)
|
||||
connect(msg.data.getString(MSG_VPN_CONFIG))
|
||||
}
|
||||
|
||||
Action.DISCONNECT -> {
|
||||
|
@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
Action.REQUEST_STATUS -> {
|
||||
clientMessenger.send {
|
||||
ServiceEvent.STATUS.packToMessage {
|
||||
putStatus(Status.build {
|
||||
setState(this@AmneziaVpnService.protocolState.value)
|
||||
})
|
||||
clientMessengers[msg.replyTo]?.let { clientMessenger ->
|
||||
clientMessenger.send {
|
||||
ServiceEvent.STATUS.packToMessage {
|
||||
putStatus(this@AmneziaVpnService.protocolState.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Action.SET_SAVE_LOGS -> {
|
||||
Log.saveLogs = msg.data.getBoolean(SAVE_LOGS)
|
||||
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +209,7 @@ class AmneziaVpnService : VpnService() {
|
|||
Log.d(TAG, "Create Amnezia VPN service")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
|
||||
clientMessenger = IpcMessenger(messengerName = "Client")
|
||||
loadServerData()
|
||||
launchProtocolStateHandler()
|
||||
networkState = NetworkState(this, ::reconnect)
|
||||
}
|
||||
|
@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
|
|||
|
||||
if (isAlwaysOnCompat) {
|
||||
Log.d(TAG, "Start service via Always-on")
|
||||
connect(Prefs.load(PREFS_CONFIG_KEY))
|
||||
connect()
|
||||
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
|
||||
Log.d(TAG, "Start service after permission check")
|
||||
connect(Prefs.load(PREFS_CONFIG_KEY))
|
||||
connect()
|
||||
} else {
|
||||
Log.d(TAG, "Start service")
|
||||
val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connect(vpnConfig)
|
||||
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
|
||||
}
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
|
||||
return START_REDELIVER_INTENT
|
||||
|
@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
|
|||
Log.d(TAG, "onBind by $intent")
|
||||
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
|
||||
isServiceBound = true
|
||||
if (isConnected) launchSendingStatistics()
|
||||
return vpnServiceMessenger.binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
Log.d(TAG, "onUnbind by $intent")
|
||||
if (intent?.action != SERVICE_INTERFACE) {
|
||||
isServiceBound = false
|
||||
stopSendingStatistics()
|
||||
clientMessenger.reset()
|
||||
if (isUnknown || isDisconnected) stopService()
|
||||
if (clientMessengers.isEmpty()) {
|
||||
isServiceBound = false
|
||||
if (isUnknown || isDisconnected) stopService()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
|
|||
Log.d(TAG, "onRebind by $intent")
|
||||
if (intent?.action != SERVICE_INTERFACE) {
|
||||
isServiceBound = true
|
||||
if (isConnected) launchSendingStatistics()
|
||||
}
|
||||
super.onRebind(intent)
|
||||
}
|
||||
|
@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
|
|||
*/
|
||||
private fun launchProtocolStateHandler() {
|
||||
mainScope.launch {
|
||||
protocolState.collect { protocolState ->
|
||||
// drop first default UNKNOWN state
|
||||
protocolState.drop(1).collect { protocolState ->
|
||||
Log.d(TAG, "Protocol state changed: $protocolState")
|
||||
when (protocolState) {
|
||||
CONNECTED -> {
|
||||
clientMessenger.send(ServiceEvent.CONNECTED)
|
||||
networkState.bindNetworkListener()
|
||||
if (isServiceBound) launchSendingStatistics()
|
||||
if (isActivityConnected) launchSendingStatistics()
|
||||
}
|
||||
|
||||
DISCONNECTED -> {
|
||||
clientMessenger.send(ServiceEvent.DISCONNECTED)
|
||||
networkState.unbindNetworkListener()
|
||||
stopSendingStatistics()
|
||||
if (!isServiceBound) stopService()
|
||||
|
@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
RECONNECTING -> {
|
||||
clientMessenger.send(ServiceEvent.RECONNECTING)
|
||||
stopSendingStatistics()
|
||||
}
|
||||
|
||||
CONNECTING, UNKNOWN -> {}
|
||||
}
|
||||
|
||||
clientMessengers.send {
|
||||
ServiceEvent.STATUS_CHANGED.packToMessage {
|
||||
putStatus(protocolState)
|
||||
}
|
||||
}
|
||||
|
||||
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
@MainThread
|
||||
private fun connect(vpnConfig: String?) {
|
||||
private fun connect(vpnConfig: String? = null) {
|
||||
if (vpnConfig == null) {
|
||||
connectToVpn(Prefs.load(PREFS_CONFIG_KEY))
|
||||
} else {
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connectToVpn(vpnConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun connectToVpn(vpnConfig: String) {
|
||||
if (isConnected || protocolState.value == CONNECTING) return
|
||||
|
||||
Log.d(TAG, "Start VPN connection")
|
||||
|
@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
|
|||
protocolState.value = CONNECTING
|
||||
|
||||
val config = parseConfigToJson(vpnConfig)
|
||||
saveServerData(config)
|
||||
if (config == null) {
|
||||
onError("Invalid VPN config")
|
||||
protocolState.value = DISCONNECTED
|
||||
|
@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
|
|||
private fun onError(msg: String) {
|
||||
Log.e(TAG, msg)
|
||||
mainScope.launch {
|
||||
clientMessenger.send {
|
||||
clientMessengers.send {
|
||||
ServiceEvent.ERROR.packToMessage {
|
||||
putString(ERROR_MSG, msg)
|
||||
putString(MSG_ERROR, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConfigToJson(vpnConfig: String?): JSONObject? =
|
||||
try {
|
||||
vpnConfig?.let {
|
||||
JSONObject(it)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
onError("Invalid VPN config json format: ${e.message}")
|
||||
private fun parseConfigToJson(vpnConfig: String): JSONObject? =
|
||||
if (vpnConfig.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
JSONObject(vpnConfig)
|
||||
} catch (e: JSONException) {
|
||||
onError("Invalid VPN config json format: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveServerData(config: JSONObject?) {
|
||||
serverName = config?.opt("description") as String?
|
||||
serverIndex = config?.opt("serverIndex") as Int? ?: -1
|
||||
Prefs.save(PREFS_SERVER_NAME, serverName)
|
||||
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
|
||||
}
|
||||
|
||||
private fun loadServerData() {
|
||||
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
|
||||
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
|
||||
}
|
||||
|
||||
private fun checkPermission(): Boolean =
|
||||
if (prepare(applicationContext) != null) {
|
||||
Intent(this, VpnRequestActivity::class.java).apply {
|
||||
|
@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
|
|||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunning(context: Context): Boolean =
|
||||
(context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
.runningAppProcesses.any {
|
||||
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,7 @@ sealed interface IpcMessage {
|
|||
}
|
||||
|
||||
enum class ServiceEvent : IpcMessage {
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
RECONNECTING,
|
||||
STATUS_CHANGED,
|
||||
STATUS,
|
||||
STATISTICS_UPDATE,
|
||||
ERROR
|
||||
|
@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
|
|||
|
||||
enum class Action : IpcMessage {
|
||||
REGISTER_CLIENT,
|
||||
UNREGISTER_CLIENT,
|
||||
CONNECT,
|
||||
DISCONNECT,
|
||||
REQUEST_STATUS,
|
||||
|
|
|
@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
|
|||
private const val TAG = "IpcMessenger"
|
||||
|
||||
class IpcMessenger(
|
||||
messengerName: String? = null,
|
||||
private val onDeadObjectException: () -> Unit = {},
|
||||
private val onRemoteException: () -> Unit = {},
|
||||
private val messengerName: String = "Unknown"
|
||||
private val onRemoteException: () -> Unit = {}
|
||||
) {
|
||||
private var messenger: Messenger? = null
|
||||
val name = messengerName ?: "Unknown"
|
||||
|
||||
constructor(
|
||||
messenger: Messenger,
|
||||
messengerName: String? = null,
|
||||
onDeadObjectException: () -> Unit = {},
|
||||
onRemoteException: () -> Unit = {}
|
||||
) : this(messengerName, onDeadObjectException, onRemoteException) {
|
||||
this.messenger = messenger
|
||||
}
|
||||
|
||||
fun set(messenger: Messenger) {
|
||||
this.messenger = messenger
|
||||
|
@ -25,19 +35,29 @@ class IpcMessenger(
|
|||
|
||||
fun send(msg: () -> Message) = messenger?.sendMsg(msg())
|
||||
|
||||
fun send(msg: Message, replyTo: Messenger) = messenger?.sendMsg(msg.apply { this.replyTo = replyTo })
|
||||
|
||||
fun <T> send(msg: T)
|
||||
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) {
|
||||
try {
|
||||
send(msg)
|
||||
} catch (e: DeadObjectException) {
|
||||
Log.w(TAG, "$messengerName messenger is dead")
|
||||
Log.w(TAG, "$name messenger is dead")
|
||||
messenger = null
|
||||
onDeadObjectException()
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(TAG, "Sending a message to the $messengerName messenger failed: ${e.message}")
|
||||
Log.w(TAG, "Sending a message to the $name messenger failed: ${e.message}")
|
||||
onRemoteException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Map<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
|
||||
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.Status
|
||||
|
||||
/**
|
||||
* JNI functions of the AndroidController class from android_controller.cpp,
|
||||
* called by events in the Android part of the client
|
||||
*/
|
||||
object QtAndroidController {
|
||||
|
||||
fun onStatus(status: Status) = onStatus(status.state)
|
||||
fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
|
||||
|
||||
external fun onStatus(stateCode: Int)
|
||||
external fun onServiceDisconnected()
|
||||
external fun onServiceError()
|
||||
|
||||
external fun onVpnPermissionRejected()
|
||||
external fun onVpnConnected()
|
||||
external fun onVpnDisconnected()
|
||||
external fun onVpnReconnecting()
|
||||
external fun onVpnStateChanged(stateCode: Int)
|
||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||
|
||||
external fun onFileOpened(uri: String)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue