diff --git a/README.md b/README.md index 6797cd9af..c183de362 100644 --- a/README.md +++ b/README.md @@ -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 ` 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__Clang_-`. +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__Clang_-/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 (`/client/android-build/.`) and you should be good to continue. ## License GPL v.3 diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 55c0deda0..16da90328 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -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 diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 22c828fb0..be67c8ba4 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -51,50 +51,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/android/shadowsocks/build.gradle b/client/android/shadowsocks/build.gradle index 69ebd0bd3..f1d738b2c 100644 --- a/client/android/shadowsocks/build.gradle +++ b/client/android/shadowsocks/build.gradle @@ -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" } diff --git a/client/android/src/org/amnezia/vpn/VPNService.kt b/client/android/src/org/amnezia/vpn/VPNService.kt index 6ca99e87d..412b1e452 100644 --- a/client/android/src/org/amnezia/vpn/VPNService.kt +++ b/client/android/src/org/amnezia/vpn/VPNService.kt @@ -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()) - } - } } diff --git a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt index 239269a5b..bc44217e1 100644 --- a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt +++ b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt @@ -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) diff --git a/client/android/src/org/amnezia/vpn/qt/CameraActivity.kt b/client/android/src/org/amnezia/vpn/qt/CameraActivity.kt index 8416d97c9..8733efa5f 100644 --- a/client/android/src/org/amnezia/vpn/qt/CameraActivity.kt +++ b/client/android/src/org/amnezia/vpn/qt/CameraActivity.kt @@ -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 { diff --git a/client/android/src/org/amnezia/vpn/qt/ImportConfigActivity.kt b/client/android/src/org/amnezia/vpn/qt/ImportConfigActivity.kt new file mode 100644 index 000000000..de175a980 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/qt/ImportConfigActivity.kt @@ -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, 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 + } + } + } +} \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt index b5e8d5fbd..d2b5b7ab5 100644 --- a/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt +++ b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt @@ -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> = 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, 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) } } diff --git a/client/main.cpp b/client/main.cpp index 62685953b..f20c5dd15 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -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); diff --git a/client/migrations.cpp b/client/migrations.cpp new file mode 100644 index 000000000..6fac6be71 --- /dev/null +++ b/client/migrations.cpp @@ -0,0 +1,86 @@ +#include "migrations.h" + +#include +#include +#include +#include + +#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 +} diff --git a/client/migrations.h b/client/migrations.h new file mode 100644 index 000000000..ea6bae92c --- /dev/null +++ b/client/migrations.h @@ -0,0 +1,24 @@ +#ifndef MIGRATIONS_H +#define MIGRATIONS_H + +#include + +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 diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index a86ab7caf..2e5641c94 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -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]() { diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 59ecd9b3d..00b372251 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -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); diff --git a/client/platforms/android/androidvpnactivity.cpp b/client/platforms/android/androidvpnactivity.cpp index 176390235..9431597bc 100644 --- a/client/platforms/android/androidvpnactivity.cpp +++ b/client/platforms/android/androidvpnactivity.cpp @@ -57,6 +57,22 @@ void AndroidVPNActivity::startQrCodeReader() QJniObject::callStaticMethod(CLASSNAME, "startQrCodeReader", "()V"); } +void AndroidVPNActivity::saveFileAs(QString fileContent, QString suggestedFilename) { + QJniObject::callStaticMethod( + CLASSNAME, + "saveFileAs", "(Ljava/lang/String;Ljava/lang/String;)V", + QJniObject::fromString(fileContent).object(), + QJniObject::fromString(suggestedFilename).object()); +} + +void AndroidVPNActivity::copyTextToClipboard(QString text) +{ + QJniObject::callStaticMethod( + CLASSNAME, + "putTextToClipboard", "(Ljava/lang/String;)V", + QJniObject::fromString(text).object()); +} + // 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( - CLASSNAME, "sendToService", "(ILjava/lang/String;)V", + CLASSNAME, + "sendToService", "(ILjava/lang/String;)V", static_cast(messageType), QJniObject::fromString(data).object()); } diff --git a/client/platforms/android/androidvpnactivity.h b/client/platforms/android/androidvpnactivity.h index 1bc1a5226..8eeb5598b 100644 --- a/client/platforms/android/androidvpnactivity.h +++ b/client/platforms/android/androidvpnactivity.h @@ -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(); diff --git a/client/ui/pages_logic/AdvancedServerSettingsLogic.cpp b/client/ui/pages_logic/AdvancedServerSettingsLogic.cpp index 0b02241c1..187af9ee5 100644 --- a/client/ui/pages_logic/AdvancedServerSettingsLogic.cpp +++ b/client/ui/pages_logic/AdvancedServerSettingsLogic.cpp @@ -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" + diff --git a/client/ui/pages_logic/GeneralSettingsLogic.cpp b/client/ui/pages_logic/GeneralSettingsLogic.cpp index cbdf26927..0e92f8c94 100644 --- a/client/ui/pages_logic/GeneralSettingsLogic.cpp +++ b/client/ui/pages_logic/GeneralSettingsLogic.cpp @@ -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())); diff --git a/client/ui/pages_logic/ServerContainersLogic.cpp b/client/ui/pages_logic/ServerContainersLogic.cpp index aeec81617..870a10dbc 100644 --- a/client/ui/pages_logic/ServerContainersLogic.cpp +++ b/client/ui/pages_logic/ServerContainersLogic.cpp @@ -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(); } diff --git a/client/ui/pages_logic/ServerListLogic.cpp b/client/ui/pages_logic/ServerListLogic.cpp index e91b1e330..79d13c8b4 100644 --- a/client/ui/pages_logic/ServerListLogic.cpp +++ b/client/ui/pages_logic/ServerListLogic.cpp @@ -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); } diff --git a/client/ui/pages_logic/StartPageLogic.cpp b/client/ui/pages_logic/StartPageLogic.cpp index 3fdfb29ba..8873f11d0 100644 --- a/client/ui/pages_logic/StartPageLogic.cpp +++ b/client/ui/pages_logic/StartPageLogic.cpp @@ -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; diff --git a/client/ui/pages_logic/StartPageLogic.h b/client/ui/pages_logic/StartPageLogic.h index b3dea0020..6f21c1050 100644 --- a/client/ui/pages_logic/StartPageLogic.h +++ b/client/ui/pages_logic/StartPageLogic.h @@ -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); diff --git a/client/ui/qml/main.qml b/client/ui/qml/main.qml index dfa33e5a0..5439ad8eb 100644 --- a/client/ui/qml/main.qml +++ b/client/ui/qml/main.qml @@ -61,8 +61,12 @@ Window { function close_page() { if (pageLoader.depth <= 1) { + if (GC.isMobile()) { + root.close() + } return } + pageLoader.currentItem.deactivated() pageLoader.pop() } diff --git a/client/ui/uilogic.cpp b/client/ui/uilogic.cpp index 527dc1a66..5a541b424 100644 --- a/client/ui/uilogic.cpp +++ b/client/ui/uilogic.cpp @@ -133,6 +133,7 @@ void UiLogic::initalizeUiLogic() connect(AndroidController::instance(), &AndroidController::initialized, [this](bool status, bool connected, const QDateTime& connectionDate) { if (connected) { pageLogic()->onConnectionStateChanged(VpnProtocol::Connected); + m_vpnConnection->restoreConnection(); } }); if (!AndroidController::instance()->initialize(pageLogic())) { @@ -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 &container) errorCode = pageLogic()->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 installedContainers; ErrorCode errorCode = m_serverController->getAlreadyInstalledContainers(credentials, installedContainers); - m_serverController->disconnectFromHost(credentials); if (errorCode != ErrorCode::NoError) { return errorCode; } diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index e01f7e616..21643ae55 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -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 diff --git a/client/vpnconnection.h b/client/vpnconnection.h index 3a0d40646..ab8f2de09 100644 --- a/client/vpnconnection.h +++ b/client/vpnconnection.h @@ -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