Merge remote-tracking branch 'remotes/origin/dev' into feature/qt6-libssh-support

This commit is contained in:
vladimir.kuznetsov 2023-04-03 17:48:55 +03:00
commit 4aba34c18b
28 changed files with 594 additions and 264 deletions

View file

@ -96,6 +96,31 @@ Build might fail with "source files not found" error the first time you try it,
dependencies in parallel, and some dependencies end up being built after the ones that
require them. In this case simply restart the build.
## How to build the Android app
_tested on Mac OS_
The Android app has the following requirements:
* JDK 11
* Android platform SDK 33
* cmake 3.25.0
After you have installed QT, QT Creator and Android Studio installed, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
* set path to jdk 11
* set path to Android SDK ($ANDROID_HOME)
In case you get errors regarding missing SDK or 'sdkmanager not running', you cannot fix them by correcting the paths and you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and click on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!
Double check that the right cmake version is configured: Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab you'll find an entry `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case click in the preferences window on the side menu item `CMake`, then on the tab `Tools`in the center content view and finally on the Button `Add` to set the path to your installed CMake.
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on on `Projects`, on the left you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that always all Android targets will be build. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (The `Details` button might be hidden in case QT Creator Window is not running in full screen!). Here we are: choose `android-33` as `Android Build platfrom SDK`.
That's it you should be ready to compile the project from QT Creator!
### Development flow
After you've hit the build button, QT-Creator copies the whole project to a folder in the repositories parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated proejct, you'll need to copy and paste the affected files to the corresponding path in the repositories Android project so that you can add and commit your changes!
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the geneated project's root directory (`<path>/client/android-build/.`) and you should be good to continue.
## License
GPL v.3

View file

@ -65,6 +65,7 @@ include_directories(
)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/migrations.h
${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc.h
${CMAKE_CURRENT_LIST_DIR}/amnezia_application.h
${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.h
@ -92,6 +93,7 @@ if(NOT IOS)
endif()
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/migrations.cpp
${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp
${CMAKE_CURRENT_LIST_DIR}/containers/containers_defs.cpp
${CMAKE_CURRENT_LIST_DIR}/core/errorstrings.cpp

View file

@ -51,50 +51,10 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.vpn"/>
<data android:pathPattern=".*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn"/>
</intent-filter>
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.cfg"/>
<data android:pathPattern=".*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg"/>
</intent-filter>
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.conf"/>
<data android:pathPattern=".*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/>
<intent-filter>
<action android:name="org.amnezia.vpn.qt.IMPORT_CONFIG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
@ -118,6 +78,56 @@
<activity
android:name=".qt.CameraActivity"
android:exported="false" />
<activity
android:name=".qt.ImportConfigActivity"
android:exported="true" >
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.vpn"/>
<data android:pathPattern=".*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn"/>
</intent-filter>
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.cfg"/>
<data android:pathPattern=".*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg"/>
</intent-filter>
<intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.conf"/>
<data android:pathPattern=".*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/>
</intent-filter>
</activity>
<service
android:name=".VPNService"

View file

@ -75,7 +75,7 @@ android {
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
@ -106,7 +106,7 @@ android {
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
androidTest.assets.srcDirs += files("${qtAndroidDir}/schemas".toString())
}
}
@ -140,7 +140,7 @@ android {
versionName "2.0.10" // Change to a higher number
javaCompileOptions.annotationProcessorOptions.arguments = [
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
"room.schemaLocation": "${qtAndroidDir}/schemas".toString()
]
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -35,7 +35,7 @@ androidExtensions {
}
//def lifecycleVersion = '2.0.0'
//def roomVersion = '2.0.0'
def roomVersion = "2.4.3"
//def preferencexVersion = '1.0.0'
dependencies {
@ -45,13 +45,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.room:room-runtime:2.2.5" // runtime
implementation "androidx.room:room-runtime:$roomVersion" // runtime
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation "androidx.browser:browser:1.3.0-alpha01"
@ -65,6 +64,7 @@ dependencies {
// api "com.takisoft.preferencex:preferencex:1.0.0"
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
api 'org.connectbot.jsocks:jsocks:1.0.0'
kapt "androidx.room:room-compiler:2.2.5"
kapt "androidx.room:room-compiler:$roomVersion"
kapt "androidx.lifecycle:lifecycle-compiler:2.4.0"
}

View file

@ -287,6 +287,8 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
}
}
set(value) {
field = value
if (value) {
mBinder.dispatchEvent(VPNServiceBinder.EVENTS.connected, "")
mConnectionTime = System.currentTimeMillis()
@ -886,45 +888,4 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
class CloseableFd(val fd: FileDescriptor) : Closeable {
override fun close() = Os.close(fd)
}
fun saveAsFile(configContent: String?, suggestedFileName: String): String {
val rootDirPath = cacheDir.absolutePath
val rootDir = File(rootDirPath)
if (!rootDir.exists()) {
rootDir.mkdirs()
}
val fileName = if (!TextUtils.isEmpty(suggestedFileName)) suggestedFileName else "amnezia.cfg"
val file = File(rootDir, fileName)
try {
file.bufferedWriter().use { out -> out.write(configContent) }
return file.toString()
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
fun shareFile(attachmentFile: String?) {
try {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/*"
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val file = File(attachmentFile)
val uri = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val createChooser = Intent.createChooser(intent, "Config sharing")
createChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(createChooser)
} catch (e: Exception) {
Log.i(tag, e.message.toString())
}
}
}

View file

@ -32,7 +32,6 @@ class VPNServiceBinder(service: VPNService) : Binder() {
const val resumeActivate = 7
const val setNotificationText = 8
const val setFallBackNotification = 9
const val shareConfig = 10
const val importConfig = 11
}
@ -139,20 +138,6 @@ class VPNServiceBinder(service: VPNService) : Binder() {
return true
}
ACTIONS.shareConfig -> {
val byteArray = data.createByteArray()
val json = byteArray?.let { String(it) }
val config = JSONObject(json)
val configContent = config.getString("data")
val suggestedName = config.getString("suggestedName")
val filePath = mService.saveAsFile(configContent, suggestedName)
Log.i(tag, "save file: $filePath")
mService.shareFile(filePath)
return true
}
ACTIONS.importConfig -> {
val buffer = data.readString()
@ -196,7 +181,6 @@ class VPNServiceBinder(service: VPNService) : Binder() {
try {
mListener?.let {
if (it.isBinderAlive) {
Log.i(tag, "Dispatching event: binder alive")
val data = Parcel.obtain()
data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
it.transact(code, data, Parcel.obtain(), 0)

View file

@ -2,11 +2,11 @@ package org.amnezia.vpn.qt
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
@ -17,9 +17,9 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import org.amnezia.vpn.R
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import org.amnezia.vpn.R
class CameraActivity : AppCompatActivity() {
@ -78,7 +78,7 @@ class CameraActivity : AppCompatActivity() {
}
}
@SuppressLint("UnsafeOptInUsageError")
@SuppressLint("UnsafeOptInUsageError", "ClickableViewAccessibility")
private fun configureVideoPreview() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
val imageCapture = ImageCapture.Builder().build()
@ -101,7 +101,20 @@ class CameraActivity : AppCompatActivity() {
try {
preview.setSurfaceProvider(viewFinder.surfaceProvider)
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, analysisUseCase)
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, analysisUseCase)
viewFinder.setOnTouchListener(View.OnTouchListener { view: View, motionEvent: MotionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> return@OnTouchListener true
MotionEvent.ACTION_UP -> {
val factory = viewFinder.meteringPointFactory
val point = factory.createPoint(motionEvent.x, motionEvent.y)
val action = FocusMeteringAction.Builder(point).build()
camera.cameraControl.startFocusAndMetering(action)
return@OnTouchListener true
}
else -> return@OnTouchListener false
}
})
} catch(exc: Exception) {
Log.e("WUTT", "Use case binding failed", exc)
}
@ -146,7 +159,6 @@ class CameraActivity : AppCompatActivity() {
}
}
}
imageProxy.close()
}
.addOnFailureListener {

View file

@ -0,0 +1,140 @@
package org.amnezia.vpn.qt
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.*
import org.amnezia.vpn.R
const val INTENT_ACTION_IMPORT_CONFIG = "org.amnezia.vpn.qt.IMPORT_CONFIG"
class ImportConfigActivity : Activity() {
private val STORAGE_PERMISSION_CODE = 42
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_import_config)
startReadConfig(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
startReadConfig(intent)
}
private fun startMainActivity(config: String?) {
if (config == null || config.length == 0) {
return
}
val activityIntent = Intent(applicationContext, VPNActivity::class.java)
activityIntent.action = INTENT_ACTION_IMPORT_CONFIG
activityIntent.addCategory("android.intent.category.DEFAULT")
activityIntent.putExtra("CONFIG", config)
startActivity(activityIntent)
finish()
}
private fun startReadConfig(intent: Intent?) {
val newIntent = intent
val newIntentAction: String = newIntent?.action ?: ""
if (newIntent != null && newIntentAction == Intent.ACTION_VIEW) {
readConfig(newIntent, newIntentAction)
}
}
private fun readConfig(newIntent: Intent, newIntentAction: String) {
if (isReadStorageAllowed()) {
val configString = processIntent(newIntent, newIntentAction)
startMainActivity(configString)
} else {
requestStoragePermission()
}
}
private fun requestStoragePermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
val configString = processIntent(intent, intent.action!!)
if (configString != null) {
startMainActivity(configString)
}
} else {
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
}
}
}
private fun processIntent(intent: Intent, action: String): String? {
val scheme = intent.scheme
if (scheme == null) {
return null
}
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
val resolver = contentResolver
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
val uri = intent.data
val name: String? = getContentName(resolver, uri)
println("Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri!!)
return input?.bufferedReader()?.use(BufferedReader::readText)
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
val uri = intent.data
val name = uri!!.lastPathSegment
println("File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri)
return input?.bufferedReader()?.use(BufferedReader::readText)
}
}
return null
}
private fun isReadStorageAllowed(): Boolean {
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
return permissionStatus == PackageManager.PERMISSION_GRANTED
}
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
val cursor = resolver!!.query(uri!!, null, null, null, null)
cursor.use {
cursor!!.moveToFirst()
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
return if (nameIndex >= 0) {
return cursor.getString(nameIndex)
} else {
null
}
}
}
}

View file

@ -5,6 +5,8 @@
package org.amnezia.vpn.qt;
import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
@ -32,11 +34,22 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private var configString: String? = null
private var vpnServiceBinder: IBinder? = null
private var isBound = false
set(value) {
field = value
if (value && configString != null) {
sendImportConfigCommand()
}
}
private val TAG = "VPNActivity"
private val STORAGE_PERMISSION_CODE = 42
private val CAMERA_ACTION_CODE = 101
private val CREATE_FILE_ACTION_CODE = 102
private var tmpFileContentToSave: String = ""
private val delayedCommands: ArrayList<Pair<Int, String>> = ArrayList()
companion object {
private lateinit var instance: VPNActivity
@ -56,19 +69,27 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
@JvmStatic fun sendToService(actionCode: Int, body: String) {
VPNActivity.getInstance().dispatchParcel(actionCode, body)
}
@JvmStatic fun saveFileAs(fileContent: String, suggestedName: String) {
VPNActivity.getInstance().saveFile(fileContent, suggestedName)
}
@JvmStatic fun putTextToClipboard(text: String) {
VPNActivity.getInstance().putToClipboard(text)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
val newIntent = intent
val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null) {
configString = processIntent(newIntent, newIntentAction)
}
super.onCreate(savedInstanceState)
instance = this
val newIntent = intent
val newIntentAction: String? = newIntent.action
if (newIntent != null && newIntentAction != null && newIntentAction == "org.amnezia.vpn.qt.IMPORT_CONFIG") {
configString = newIntent.getStringExtra("CONFIG")
}
}
private fun startQrCodeActivity() {
@ -76,6 +97,18 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
startActivityForResult(intent, CAMERA_ACTION_CODE)
}
private fun saveFile(fileContent: String, suggestedName: String) {
tmpFileContentToSave = fileContent
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, CREATE_FILE_ACTION_CODE)
}
override fun getSystemService(name: String): Any? {
return if (Build.VERSION.SDK_INT >= 29 && name == "clipboard") {
// QT will always attempt to read the clipboard if content is there.
@ -98,11 +131,22 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private fun dispatchParcel(actionCode: Int, body: String) {
if (!isBound) {
Log.d(TAG, "dispatchParcel: not bound")
delayedCommands.add(Pair(actionCode, body))
return
} else {
Log.d(TAG, "dispatchParcel: bound")
}
if (delayedCommands.size > 0) {
for (command in delayedCommands) {
processCommand(command.first, command.second)
}
delayedCommands.clear()
}
processCommand(actionCode, body)
}
private fun processCommand(actionCode: Int, body: String) {
val out: Parcel = Parcel.obtain()
out.writeByteArray(body.toByteArray())
@ -118,19 +162,15 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
}
override fun onNewIntent(newIntent: Intent) {
intent = newIntent
super.onNewIntent(intent)
setIntent(newIntent)
val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null && newIntentAction != Intent.ACTION_MAIN) {
if (isReadStorageAllowed()) {
configString = processIntent(newIntent, newIntentAction)
} else {
requestStoragePermission()
}
}
super.onNewIntent(intent)
if (newIntent != null && newIntentAction != null && newIntentAction == INTENT_ACTION_IMPORT_CONFIG) {
configString = newIntent.getStringExtra("CONFIG")
}
}
override fun onResume() {
@ -141,84 +181,6 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
}
}
private fun isReadStorageAllowed(): Boolean {
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
return permissionStatus == PackageManager.PERMISSION_GRANTED
}
private fun requestStoragePermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Storage read permission granted")
if (configString == null) {
configString = processIntent(intent, intent.action!!)
}
if (configString != null) {
Log.d(TAG, "not empty")
sendImportConfigCommand()
} else {
Log.d(TAG, "empty")
}
} else {
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
}
}
}
private fun processIntent(intent: Intent, action: String): String? {
val scheme = intent.scheme
if (scheme == null) {
return null
}
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
val resolver = contentResolver
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
val uri = intent.data
val name: String? = getContentName(resolver, uri)
Log.d(TAG, "Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri!!)
return input?.bufferedReader()?.use(BufferedReader::readText)
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
val uri = intent.data
val name = uri!!.lastPathSegment
Log.d(TAG, "File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri)
return input?.bufferedReader()?.use(BufferedReader::readText)
}
}
return null
}
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
val cursor = resolver!!.query(uri!!, null, null, null, null)
cursor.use {
cursor!!.moveToFirst()
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
return if (nameIndex >= 0) {
return cursor.getString(nameIndex)
} else {
null
}
}
}
private fun sendImportConfigCommand() {
if (configString != null) {
val msg: Parcel = Parcel.obtain()
@ -234,7 +196,7 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
}
}
private var connection: ServiceConnection = object : ServiceConnection {
private fun createConnection() = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
vpnServiceBinder = binder
@ -260,6 +222,8 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
}
}
private var connection: ServiceConnection = createConnection()
private fun registerBinder(): Boolean {
val binder = VPNClientBinder()
val out: Parcel = Parcel.obtain()
@ -330,13 +294,38 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
val extra = data?.getStringExtra("result") ?: ""
onActivityMessage(UI_EVENT_QR_CODE_RECEIVED, extra)
}
if (requestCode == CREATE_FILE_ACTION_CODE && resultCode == RESULT_OK) {
data?.data?.also { uri ->
alterDocument(uri)
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) {
onBackPressed()
return true
private fun alterDocument(uri: Uri) {
try {
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
FileOutputStream(fd.fileDescriptor).use { fos ->
fos.write(tmpFileContentToSave.toByteArray())
}
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
tmpFileContentToSave = ""
}
private fun putToClipboard(text: String) {
this.runOnUiThread {
val clipboard = applicationContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?
if (clipboard != null) {
val clip: ClipData = ClipData.newPlainText("", text)
clipboard.setPrimaryClip(clip)
}
}
return super.onKeyDown(keyCode, event)
}
}

View file

@ -4,6 +4,7 @@
#include "amnezia_application.h"
#include "defines.h"
#include "migrations.h"
#ifdef Q_OS_WIN
#include "Windows.h"
@ -16,6 +17,9 @@
int main(int argc, char *argv[])
{
Migrations migrationsManager;
migrationsManager.doMigrations();
QLoggingCategory::setFilterRules(QStringLiteral("qtc.ssh=false"));
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);

86
client/migrations.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "migrations.h"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include "defines.h"
Migrations::Migrations(QObject *parent)
: QObject{parent}
{
QString version(APP_MAJOR_VERSION);
QStringList versionDigits = version.split(".");
if (versionDigits.size() >= 3) {
currentMajor = versionDigits[0].toInt();
currentMinor = versionDigits[1].toInt();
currentMicro = versionDigits[2].toInt();
}
if (versionDigits.size() == 4) {
currentPatch = versionDigits[3].toInt();
}
}
void Migrations::doMigrations()
{
if (currentMajor == 3) {
migrateV3();
}
}
void Migrations::migrateV3()
{
#ifdef Q_OS_ANDROID
qDebug() << "Migration to V3 on Android...";
QString packageName = "org.amnezia.vpn";
QDir dir(".");
QString currentDir = dir.absolutePath();
int packageNameIndex = currentDir.indexOf(packageName);
if (packageNameIndex == -1) {
return;
}
QString rootLocation = currentDir.left(packageNameIndex + packageName.size());
if (rootLocation.isEmpty()) {
return;
}
QString location = rootLocation + "/files/.config/AmneziaVPN.ORG/AmneziaVPN.conf";
QFile oldConfig(location);
if (oldConfig.exists()) {
QString newConfigPath = rootLocation + "/files/settings";
QDir newConfigDir(newConfigPath);
newConfigPath += "/AmneziaVPN.ORG";
bool mkPathRes = newConfigDir.mkpath(newConfigPath);
if (!mkPathRes) {
return;
}
QFile newConfigFile(newConfigPath + "/AmneziaVPN.conf");
if (!newConfigFile.exists()) {
bool cpResult = QFile::copy(oldConfig.fileName(), newConfigFile.fileName());
if (cpResult) {
oldConfig.remove();
QDir oldConfigDir(rootLocation + "/files/.config");
oldConfigDir.rmdir("AmneziaVPN.ORG");
}
}
}
#endif
}

24
client/migrations.h Normal file
View file

@ -0,0 +1,24 @@
#ifndef MIGRATIONS_H
#define MIGRATIONS_H
#include <QObject>
class Migrations : public QObject
{
Q_OBJECT
public:
explicit Migrations(QObject *parent = nullptr);
void doMigrations();
private:
void migrateV3();
private:
int currentMajor = 0;
int currentMinor = 0;
int currentMicro = 0;
int currentPatch = 0;
};
#endif // MIGRATIONS_H

View file

@ -15,6 +15,7 @@
#include "private/qandroidextras_p.h"
#include "ui/pages_logic/StartPageLogic.h"
#include "androidvpnactivity.h"
#include "androidutils.h"
namespace {
@ -54,6 +55,10 @@ AndroidController::AndroidController() : QObject()
isConnected = doc.object()["connected"].toBool();
if (isConnected) {
emit scheduleStatusCheckSignal();
}
emit initialized(
true, isConnected,
time > 0 ? QDateTime::fromMSecsSinceEpoch(time) : QDateTime());
@ -66,9 +71,11 @@ AndroidController::AndroidController() : QObject()
Q_UNUSED(parcelBody);
qDebug() << "Transact: connected";
isConnected = true;
if (!isConnected) {
emit scheduleStatusCheckSignal();
}
emit scheduleStatusCheckSignal();
isConnected = true;
emit connectionStateChanged(VpnProtocol::Connected);
}, Qt::QueuedConnection);
@ -85,7 +92,6 @@ AndroidController::AndroidController() : QObject()
connect(activity, &AndroidVPNActivity::eventStatisticUpdate, this,
[this](const QString& parcelBody) {
qDebug() << "Transact: update";
auto doc = QJsonDocument::fromJson(parcelBody.toUtf8());
QString rx = doc.object()["rx_bytes"].toString();
@ -203,12 +209,7 @@ void AndroidController::setNotificationText(const QString& title,
}
void AndroidController::shareConfig(const QString& configContent, const QString& suggestedName) {
QJsonObject rootObject;
rootObject["data"] = configContent;
rootObject["suggestedName"] = suggestedName;
QJsonDocument doc(rootObject);
AndroidVPNActivity::sendToService(ServiceAction::ACTION_SHARE_CONFIG, doc.toJson());
AndroidVPNActivity::saveFileAs(configContent, suggestedName);
}
/*
@ -248,7 +249,7 @@ void AndroidController::cleanupBackendLogs() {
}
void AndroidController::importConfig(const QString& data){
m_startPageLogic->importConnectionFromCode(data);
m_startPageLogic->selectConfigFormat(data);
}
const QJsonObject &AndroidController::vpnConfig() const
@ -266,6 +267,11 @@ void AndroidController::startQrReaderActivity()
AndroidVPNActivity::instance()->startQrCodeReader();
}
void AndroidController::copyTextToClipboard(QString text)
{
AndroidVPNActivity::instance()->copyTextToClipboard(text);
}
void AndroidController::scheduleStatusCheckSlot()
{
QTimer::singleShot(1000, [this]() {

View file

@ -11,7 +11,6 @@
#include "ui/pages_logic/StartPageLogic.h"
#include "protocols/vpnprotocol.h"
#include "androidvpnactivity.h"
using namespace amnezia;
@ -44,6 +43,7 @@ public:
void setVpnConfig(const QJsonObject &newVpnConfig);
void startQrReaderActivity();
void copyTextToClipboard(QString text);
signals:
void connectionStateChanged(VpnProtocol::VpnConnectionState state);

View file

@ -57,6 +57,22 @@ void AndroidVPNActivity::startQrCodeReader()
QJniObject::callStaticMethod<void>(CLASSNAME, "startQrCodeReader", "()V");
}
void AndroidVPNActivity::saveFileAs(QString fileContent, QString suggestedFilename) {
QJniObject::callStaticMethod<void>(
CLASSNAME,
"saveFileAs", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(fileContent).object<jstring>(),
QJniObject::fromString(suggestedFilename).object<jstring>());
}
void AndroidVPNActivity::copyTextToClipboard(QString text)
{
QJniObject::callStaticMethod<void>(
CLASSNAME,
"putTextToClipboard", "(Ljava/lang/String;)V",
QJniObject::fromString(text).object<jstring>());
}
// static
AndroidVPNActivity* AndroidVPNActivity::instance() {
if (s_instance == nullptr) {
@ -70,9 +86,9 @@ AndroidVPNActivity* AndroidVPNActivity::instance() {
void AndroidVPNActivity::sendToService(ServiceAction type, const QString& data) {
int messageType = (int)type;
QJniEnvironment env;
QJniObject::callStaticMethod<void>(
CLASSNAME, "sendToService", "(ILjava/lang/String;)V",
CLASSNAME,
"sendToService", "(ILjava/lang/String;)V",
static_cast<int>(messageType),
QJniObject::fromString(data).object<jstring>());
}

View file

@ -75,6 +75,8 @@ public:
static void sendToService(ServiceAction type, const QString& data);
static void connectService();
static void startQrCodeReader();
static void saveFileAs(QString fileContent, QString suggestedFilename);
static void copyTextToClipboard(QString text);
signals:
void serviceConnected();

View file

@ -45,7 +45,6 @@ void AdvancedServerSettingsLogic::onPushButtonClearServerClicked()
}
ErrorCode e = m_serverController->removeAllContainers(m_settings->serverCredentials(uiLogic()->m_selectedServerIndex));
m_serverController->disconnectFromHost(m_settings->serverCredentials(uiLogic()->m_selectedServerIndex));
if (e) {
emit uiLogic()->showWarningMessage(tr("Error occurred while cleaning the server.") + "\n" +
tr("Error message: ") + errorString(e) + "\n" +

View file

@ -13,7 +13,7 @@ GeneralSettingsLogic::GeneralSettingsLogic(UiLogic *logic, QObject *parent):
void GeneralSettingsLogic::onUpdatePage()
{
uiLogic()->m_selectedServerIndex = m_settings->defaultServerIndex();
set_existsAnyServer(uiLogic()->m_selectedServerIndex >= 0);
set_existsAnyServer(m_settings->serversCount() > 0);
uiLogic()->m_selectedDockerContainer = m_settings->defaultContainer(m_settings->defaultServerIndex());
set_pushButtonGeneralSettingsShareConnectionEnable(m_settings->haveAuthData(m_settings->defaultServerIndex()));

View file

@ -31,6 +31,7 @@ void ServerContainersLogic::onUpdatePage()
p_model->setSelectedServerIndex(uiLogic()->m_selectedServerIndex);
set_isManagedServer(m_settings->haveAuthData(uiLogic()->m_selectedServerIndex));
uiLogic()->m_installCredentials = m_settings->serverCredentials(uiLogic()->m_selectedServerIndex);
emit updatePage();
}

View file

@ -21,7 +21,6 @@ void ServerListLogic::onServerListPushbuttonDefaultClicked(int index)
void ServerListLogic::onServerListPushbuttonSettingsClicked(int index)
{
uiLogic()->m_selectedServerIndex = index;
uiLogic()->m_installCredentials = m_settings->serverCredentials(index);
uiLogic()->goToPage(Page::ServerSettings);
}

View file

@ -181,14 +181,7 @@ void StartPageLogic::onPushButtonImportOpenFile()
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
auto configFormat = checkConfigFormat(QString(data));
if (configFormat == ConfigTypes::OpenVpn) {
importConnectionFromOpenVpnConfig(QString(data));
} else if (configFormat == ConfigTypes::WireGuard) {
importConnectionFromWireguardConfig(QString(data));
} else {
importConnectionFromCode(QString(data));
}
selectConfigFormat(QString(data));
}
#ifdef Q_OS_ANDROID
@ -198,6 +191,18 @@ void StartPageLogic::startQrDecoder()
}
#endif
void StartPageLogic::selectConfigFormat(QString configData)
{
auto configFormat = checkConfigFormat(configData);
if (configFormat == ConfigTypes::OpenVpn) {
importConnectionFromOpenVpnConfig(configData);
} else if (configFormat == ConfigTypes::WireGuard) {
importConnectionFromWireguardConfig(configData);
} else {
importConnectionFromCode(configData);
}
}
bool StartPageLogic::importConnection(const QJsonObject &profile)
{
ServerCredentials credentials;

View file

@ -35,6 +35,8 @@ public:
Q_INVOKABLE void startQrDecoder();
#endif
void selectConfigFormat(QString configData);
bool importConnection(const QJsonObject &profile);
bool importConnectionFromCode(QString code);
bool importConnectionFromQr(const QByteArray &data);

View file

@ -61,8 +61,12 @@ Window {
function close_page() {
if (pageLoader.depth <= 1) {
if (GC.isMobile()) {
root.close()
}
return
}
pageLoader.currentItem.deactivated()
pageLoader.pop()
}

View file

@ -133,6 +133,7 @@ void UiLogic::initalizeUiLogic()
connect(AndroidController::instance(), &AndroidController::initialized, [this](bool status, bool connected, const QDateTime& connectionDate) {
if (connected) {
pageLogic<VpnLogic>()->onConnectionStateChanged(VpnProtocol::Connected);
m_vpnConnection->restoreConnection();
}
});
if (!AndroidController::instance()->initialize(pageLogic<StartPageLogic>())) {
@ -234,9 +235,10 @@ void UiLogic::keyPressEvent(Qt::Key key)
m_configurator->sshConfigurator->openSshTerminal(m_settings->serverCredentials(m_settings->defaultServerIndex()));
break;
case Qt::Key_Escape:
case Qt::Key_Back:
if (currentPage() == Page::Vpn) break;
if (currentPage() == Page::ServerConfiguringProgress) break;
case Qt::Key_Back:
// if (currentPage() == Page::Start && pagesStack.size() < 2) break;
// if (currentPage() == Page::Sites &&
// ui->tableView_sites->selectionModel()->selection().indexes().size() > 0) {
@ -253,10 +255,16 @@ void UiLogic::keyPressEvent(Qt::Key key)
void UiLogic::onCloseWindow()
{
if (m_settings->serversCount() == 0) qApp->quit();
else {
hide();
#ifdef Q_OS_ANDROID
qApp->quit();
#else
if (m_settings->serversCount() == 0)
{
qApp->quit();
} else {
emit hide();
}
#endif
}
QString UiLogic::containerName(int container)
@ -344,8 +352,6 @@ void UiLogic::installServer(QPair<DockerContainer, QJsonObject> &container)
errorCode = pageLogic<ServerConfiguringProgressLogic>()->doInstallAction(installAction, pageFunc, progressBarFunc,
noButton, waitInfoFunc,
busyInfoFunc, cancelButtonFunc);
m_serverController->disconnectFromHost(m_installCredentials);
if (errorCode == ErrorCode::NoError) {
if (!isServerCreated) {
QJsonObject server;
@ -476,7 +482,11 @@ void UiLogic::saveBinaryFile(const QString &desc, QString ext, const QString &da
void UiLogic::copyToClipboard(const QString &text)
{
#ifdef Q_OS_ANDROID
AndroidController::instance()->copyTextToClipboard(text);
#else
qApp->clipboard()->setText(text);
#endif
}
void UiLogic::shareTempFile(const QString &suggestedName, QString ext, const QString& data) {
@ -532,7 +542,6 @@ ErrorCode UiLogic::addAlreadyInstalledContainersGui(bool createNewServer, bool &
QMap<DockerContainer, QJsonObject> installedContainers;
ErrorCode errorCode = m_serverController->getAlreadyInstalledContainers(credentials, installedContainers);
m_serverController->disconnectFromHost(credentials);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}

View file

@ -20,7 +20,6 @@
#ifdef Q_OS_ANDROID
#include "../../platforms/android/android_controller.h"
#include "protocols/android_vpnprotocol.h"
#endif
#ifdef Q_OS_IOS
@ -353,10 +352,8 @@ void VpnConnection::connectToVpn(int serverIndex,
}
m_vpnProtocol->prepare();
#elif defined Q_OS_ANDROID
Proto proto = ContainerProps::defaultProtocol(container);
AndroidVpnProtocol *androidVpnProtocol = new AndroidVpnProtocol(proto, m_vpnConfiguration);
connect(AndroidController::instance(), &AndroidController::connectionStateChanged, androidVpnProtocol, &AndroidVpnProtocol::setConnectionState);
connect(AndroidController::instance(), &AndroidController::statusUpdated, androidVpnProtocol, &AndroidVpnProtocol::connectionDataUpdated);
androidVpnProtocol = createDefaultAndroidVpnProtocol(container);
createAndroidConnections(container);
m_vpnProtocol.reset(androidVpnProtocol);
#elif defined Q_OS_IOS
@ -373,16 +370,52 @@ void VpnConnection::connectToVpn(int serverIndex,
m_vpnProtocol.reset(iosVpnProtocol);
#endif
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(VpnProtocol::VpnConnectionState)), this, SLOT(onConnectionStateChanged(VpnProtocol::VpnConnectionState)));
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
m_serverController->disconnectFromHost(credentials);
createProtocolConnections();
e = m_vpnProtocol.data()->start();
if (e) emit VpnProtocol::Error;
}
void VpnConnection::createProtocolConnections() {
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(VpnProtocol::VpnConnectionState)), this, SLOT(onConnectionStateChanged(VpnProtocol::VpnConnectionState)));
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
}
#ifdef Q_OS_ANDROID
void VpnConnection::restoreConnection() {
createAndroidConnections();
m_vpnProtocol.reset(androidVpnProtocol);
createProtocolConnections();
}
void VpnConnection::createAndroidConnections()
{
int serverIndex = m_settings->defaultServerIndex();
DockerContainer container = m_settings->defaultContainer(serverIndex);
createAndroidConnections(container);
}
void VpnConnection::createAndroidConnections(DockerContainer container)
{
androidVpnProtocol = createDefaultAndroidVpnProtocol(container);
connect(AndroidController::instance(), &AndroidController::connectionStateChanged, androidVpnProtocol, &AndroidVpnProtocol::setConnectionState);
connect(AndroidController::instance(), &AndroidController::statusUpdated, androidVpnProtocol, &AndroidVpnProtocol::connectionDataUpdated);
}
AndroidVpnProtocol* VpnConnection::createDefaultAndroidVpnProtocol(DockerContainer container)
{
Proto proto = ContainerProps::defaultProtocol(container);
AndroidVpnProtocol *androidVpnProtocol = new AndroidVpnProtocol(proto, m_vpnConfiguration);
return androidVpnProtocol;
}
#endif
QString VpnConnection::bytesPerSecToText(quint64 bytes)
{
double mbps = bytes * 8 / 1e6;
@ -401,8 +434,6 @@ void VpnConnection::disconnectFromVpn()
}
#endif
if (!m_vpnProtocol.data()) {
emit connectionStateChanged(VpnProtocol::Disconnected);
#ifdef Q_OS_ANDROID
@ -415,11 +446,8 @@ void VpnConnection::disconnectFromVpn()
VpnProtocol::VpnConnectionState VpnConnection::connectionState()
{
if (!m_vpnProtocol) return VpnProtocol::Disconnected;
return m_vpnProtocol->connectionState();
}
bool VpnConnection::isConnected() const

View file

@ -18,6 +18,10 @@
#include "core/ipcclient.h"
#endif
#ifdef Q_OS_ANDROID
#include "protocols/android_vpnprotocol.h"
#endif
class VpnConfigurator;
class ServerController;
@ -61,6 +65,10 @@ public:
const QString &remoteAddress() const;
void addSitesRoutes(const QString &gw, Settings::RouteMode mode);
#ifdef Q_OS_ANDROID
void restoreConnection();
#endif
public slots:
void connectToVpn(int serverIndex,
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig);
@ -101,6 +109,15 @@ private:
#ifdef Q_OS_IOS
IOSVpnProtocol * iosVpnProtocol{nullptr};
#endif
#ifdef Q_OS_ANDROID
AndroidVpnProtocol* androidVpnProtocol = nullptr;
AndroidVpnProtocol* createDefaultAndroidVpnProtocol(DockerContainer container);
void createAndroidConnections();
void createAndroidConnections(DockerContainer container);
#endif
void createProtocolConnections();
};
#endif // VPNCONNECTION_H