diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 27d17c0dc..397dc18a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -255,7 +255,7 @@ jobs: host: 'linux' target: 'desktop' arch: 'gcc_64' - modules: 'qtremoteobjects qt5compat' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' dir: ${{ runner.temp }} setup-python: 'true' set-env: 'true' @@ -268,7 +268,7 @@ jobs: host: 'linux' target: 'android' arch: ${{ matrix.arch }} - modules: 'qtremoteobjects qt5compat' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' dir: ${{ runner.temp }} setup-python: 'true' set-env: 'true' @@ -287,17 +287,19 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '8' + java-version: '11' - name: 'Build project' run: | - export NDK_VERSION=21d - export ANDROID_NDK_PLATFORM=android-21 + export QT_HOST_PATH="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64" + export NDK_VERSION=23c + export ANDROID_NDK_PLATFORM=android-23 export ANDROID_NDK_HOME=${{ runner.temp }}/android-ndk-r${NDK_VERSION} export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME + export ANDROID_CURRENT_ARCH=${{ matrix.arch }} if [ ! -f $ANDROID_NDK_ROOT/ndk-build ]; then - wget https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux-x86_64.zip -qO ${{ runner.temp }}/ndk.zip && + wget https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip -qO ${{ runner.temp }}/ndk.zip && unzip -q -d ${{ runner.temp }} ${{ runner.temp }}/ndk.zip ; fi diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml new file mode 100644 index 000000000..b88390f49 --- /dev/null +++ b/.github/workflows/tag-deploy.yml @@ -0,0 +1,142 @@ +name: 'Release deploy workflow' + +on: + workflow_dispatch: + # push: + # tags: + # - ** + +jobs: + + Build-Android-Release: + name: 'Build-Android-Release' + runs-on: ubuntu-latest + + env: + QT_VERSION: 6.4.1 + QIF_VERSION: 4.5 + + steps: + - name: 'Install desktop Qt' + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'desktop' + arch: 'gcc_64' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + dir: ${{ runner.temp }} + setup-python: 'true' + set-env: 'true' + extra: '--external 7z' + + - name: 'Install android Qt x86_64' + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'android' + arch: 'android_x86_64' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + dir: ${{ runner.temp }} + setup-python: 'true' + set-env: 'true' + extra: '--external 7z' + + - name: 'Install android Qt x86' + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'android' + arch: 'android_x86' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + dir: ${{ runner.temp }} + setup-python: 'true' + set-env: 'true' + extra: '--external 7z' + + - name: 'Install android Qt arm_v7' + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'android' + arch: 'android_armv7' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + dir: ${{ runner.temp }} + setup-python: 'true' + set-env: 'true' + extra: '--external 7z' + + - name: 'Install android Qt arm_v8' + uses: jurplel/install-qt-action@v3 + with: + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'android' + arch: 'android_arm64_v8a' + modules: 'qtremoteobjects qt5compat qtimageformats qtshadertools' + dir: ${{ runner.temp }} + setup-python: 'true' + set-env: 'true' + extra: '--external 7z' + + - name: 'Get sources' + uses: actions/checkout@v3 + with: + path: main + submodules: 'true' + fetch-depth: 10 + + - name: 'Preparations before keystore fetching' + run: | + mkdir keystore + + - name: 'Getting keystore' + uses: actions/checkout@v3 + with: + repository: amnezia-vpn/amnezia-android-certificates + ssh-key: ${{ secrets.ANDROID_CERTS_SSH_PRIVATE_KEY }} + path: keystore + + - name: 'Setup ccache' + uses: hendrikmuhs/ccache-action@v1.2 + + - name: 'Setup Java' + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: 'Build project' + run: | + export QT_HOST_PATH="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64" + export NDK_VERSION=23c + export ANDROID_NDK_PLATFORM=android-23 + export ANDROID_NDK_HOME=${{ runner.temp }}/android-ndk-r${NDK_VERSION} + export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME + + if [ ! -f $ANDROID_NDK_ROOT/ndk-build ]; then + wget https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}-linux.zip -qO ${{ runner.temp }}/ndk.zip && + unzip -q -d ${{ runner.temp }} ${{ runner.temp }}/ndk.zip ; + fi + + export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_arm64_v8a/bin + cd main + bash deploy/build_android.sh + + - name: 'Signing APK' + run: | + pwd + + ANDROID_BUILD_TOOLS_VERSION=30.0.3 + + ${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS_VERSION}/zipalign -f -v 4 AmneziaVPN-release-unsigned.apk AmneziaVPN-release-aligned.apk + ${ANDROID_HOME}/build-tools/${ANDROID_BUILD_TOOLS_VERSION}/apksigner sign --out AmneziaVPN-release-signed.apk --ks keystore/debug.keystore --ks-key-alias ${{ secrets.DEBUG_ANDROID_KEYSTORE_KEY_ALIAS }} --ks-pass pass:${{secrets.DEBUG_ANDROID_KEYSTOTE_KEY_PASS }} AmneziaVPN-release-aligned.apk + + - name: 'Upload' + uses: actions/upload-artifact@v3 + with: + name: Release APK + path: ${{ runner.temp }}/main/AmneziaVPN-release-signed.apk diff --git a/CMakeLists.txt b/CMakeLists.txt index 6236396e2..fa8418198 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.23.0 FATAL_ERROR) set(PROJECT AmneziaVPN) project(${PROJECT}) +if(ANDROID) + set(QT_ANDROID_BUILD_ALL_ABIS ON) +endif() + add_subdirectory(client) if(NOT IOS AND NOT ANDROID) diff --git a/client/3rd/QtSsh/src/botan/botan.cmake b/client/3rd/QtSsh/src/botan/botan.cmake index d363fe714..24a35d1c2 100644 --- a/client/3rd/QtSsh/src/botan/botan.cmake +++ b/client/3rd/QtSsh/src/botan/botan.cmake @@ -38,11 +38,13 @@ if(ANDROID) # We need to include qtprivate api's # As QAndroidBinder is not yet implemented with a public api set(LIBS ${LIBS} Qt6::CorePrivate) - set(ANDROID_ABIS ANDROID_TARGET_ARCH) - link_directories(${CMAKE_CURRENT_LIST_DIR}/android/${ANDROID_TARGET_ARCH}) - set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/android/${ANDROID_TARGET_ARCH}/botan_all.h) - set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/android/${ANDROID_TARGET_ARCH}/botan_all.cpp) + set(abi ${CMAKE_ANDROID_ARCH_ABI}) + + include_directories(${CMAKE_CURRENT_LIST_DIR}/android/${abi}) + link_directories(${CMAKE_CURRENT_LIST_DIR}/android/${abi}) + set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/android/${abi}/botan_all.h) + set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/android/${abi}/botan_all.cpp) endif() if(IOS) @@ -66,8 +68,4 @@ if(IOS) include_directories(${CMAKE_CURRENT_LIST_DIR}/ios/iphone) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/ios/iphone/botan_all.h) set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/ios/iphone/botan_all.cpp) - - - - endif() diff --git a/client/3rd/qtkeychain b/client/3rd/qtkeychain index f197cdb93..c6f0b6631 160000 --- a/client/3rd/qtkeychain +++ b/client/3rd/qtkeychain @@ -1 +1 @@ -Subproject commit f197cdb935b0cfd9881fdc6860874cb8379d1238 +Subproject commit c6f0b66318f8da6917fb4681103f7303b1836194 diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 825ee3d08..004385b72 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -266,7 +266,6 @@ if(ANDROID) # We need to include qtprivate api's # As QAndroidBinder is not yet implemented with a public api set(LIBS ${LIBS} Qt6::CorePrivate) - set(ANDROID_ABIS ANDROID_TARGET_ARCH) add_compile_definitions(MVPN_ANDROID) @@ -275,12 +274,16 @@ if(ANDROID) set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_LIST_DIR}/platforms/android/android_notificationhandler.h + ${CMAKE_CURRENT_LIST_DIR}/platforms/android/androidutils.h + ${CMAKE_CURRENT_LIST_DIR}/platforms/android/androidvpnactivity.h ${CMAKE_CURRENT_LIST_DIR}/protocols/android_vpnprotocol.h ) set(SOURCES ${SOURCES} - ${CMAKE_CURRENT_LIST_DIR}/platforms/android/android_controller.cp + ${CMAKE_CURRENT_LIST_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_LIST_DIR}/platforms/android/android_notificationhandler.cpp + ${CMAKE_CURRENT_LIST_DIR}/platforms/android/androidutils.cpp + ${CMAKE_CURRENT_LIST_DIR}/platforms/android/androidvpnactivity.cpp ${CMAKE_CURRENT_LIST_DIR}/protocols/android_vpnprotocol.cpp ) endif() @@ -482,25 +485,26 @@ if(ANDROID) ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/NotificationUtil.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/OpenVPNThreadv3.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/Prefs.kt - ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VpnLogger.kt - ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VpnService.kt - ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VpnServiceBinder.kt + ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VPNLogger.kt + ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VPNService.kt + ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/VPNServiceBinder.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/AmneziaApp.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/PackageManagerHelper.java ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNActivity.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNApplication.java + ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNClientBinder.kt ${CMAKE_CURRENT_LIST_DIR}/android/src/org/amnezia/vpn/qt/VPNPermissionHelper.kt ${CMAKE_CURRENT_BINARY_DIR} ) - set_property(TARGET ${PROJECT} + set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/android ) - foreach(abi IN ANDROID_ABIS) - if(ANDROID_TARGET_ARCH EQUAL ${abi}) - set(LIBS ${LIBS} + foreach(abi IN ITEMS ${QT_ANDROID_ABIS}) + if(CMAKE_ANDROID_ARCH_ABI STREQUAL ${abi}) + set(LIBS ${LIBS} ${CMAKE_CURRENT_LIST_DIR}/3rd/OpenSSL/lib/android/${abi}/libcrypto.a ${CMAKE_CURRENT_LIST_DIR}/3rd/OpenSSL/lib/android/${abi}/libssl.a ) @@ -510,7 +514,7 @@ if(ANDROID) ${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg.so ${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg-go.so ${CMAKE_CURRENT_LIST_DIR}/android/lib/wireguard/${abi}/libwg-quick.so - + ${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libjbcrypto.so ${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libopenvpn.so ${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libopvpnutil.so @@ -518,6 +522,7 @@ if(ANDROID) ${CMAKE_CURRENT_LIST_DIR}/android/lib/openvpn/${abi}/libovpnexec.so ) endforeach() + endif() target_link_libraries(${PROJECT} PRIVATE ${LIBS}) diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 55cffe204..09a8775b6 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -21,13 +21,19 @@ Remove the comment if you do not require these default features. --> - + + android:exported="true"> + + - + - - - + + + - + @@ -58,14 +65,14 @@ - + - - - + + + - + @@ -73,14 +80,14 @@ - + - - - + + + - + @@ -88,102 +95,51 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + - - - - - - - - - - - - - - - - + + + + + - - + + - - - + diff --git a/client/android/build.gradle b/client/android/build.gradle index 121297202..69eda3e58 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -20,7 +20,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" @@ -72,7 +72,11 @@ android { compileSdkVersion androidCompileSdkVersion.toInteger() - //buildToolsVersion '28.0.3' + buildToolsVersion androidBuildToolsVersion + ndkVersion androidNdkVersion + + // Extract native libraries from the APK + packagingOptions.jniLibs.useLegacyPackaging true dexOptions { javaMaxHeapSize "3g" @@ -81,9 +85,9 @@ android { sourceSets { main { manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] - aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] - res.srcDirs = [qt5AndroidDir + '/res', 'res'] + java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qtAndroidDir + '/res', 'res'] resources.srcDirs = ['resources'] renderscript.srcDirs = ['src'] assets.srcDirs = ['assets'] @@ -139,6 +143,7 @@ android { debug { //applicationIdSuffix ".debug" //versionNameSuffix "-debug" + minifyEnabled false externalNativeBuild { cmake { arguments "-DANDROID_PACKAGE_NAME=${groupName}", "-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}" diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 4e1cc9db6..669386b87 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/client/android/res/values/libs.xml b/client/android/res/values/libs.xml index 6b1a4a2a0..fe63866f7 100644 --- a/client/android/res/values/libs.xml +++ b/client/android/res/values/libs.xml @@ -1,11 +1,6 @@ - - https://download.qt.io/ministro/android/qt5/qt-5.14 - - - + @@ -19,4 +14,8 @@ + + + + diff --git a/client/android/src/org/amnezia/vpn/AuthHelper.java b/client/android/src/org/amnezia/vpn/AuthHelper.java index b30aa8f50..940d03c27 100644 --- a/client/android/src/org/amnezia/vpn/AuthHelper.java +++ b/client/android/src/org/amnezia/vpn/AuthHelper.java @@ -3,7 +3,7 @@ package org.amnezia.vpn; import android.content.Context; import android.app.KeyguardManager; import android.content.Intent; -import org.qtproject.qt5.android.bindings.QtActivity; +import org.qtproject.qt.android.bindings.QtActivity; import static android.content.Context.KEYGUARD_SERVICE; diff --git a/client/android/src/org/amnezia/vpn/OpenVPNThreadv3.kt b/client/android/src/org/amnezia/vpn/OpenVPNThreadv3.kt index f40f95365..39e077dd1 100644 --- a/client/android/src/org/amnezia/vpn/OpenVPNThreadv3.kt +++ b/client/android/src/org/amnezia/vpn/OpenVPNThreadv3.kt @@ -31,6 +31,31 @@ class OpenVPNThreadv3(var service: VPNService): ClientAPI_OpenVPNClient(), Runna private var mAlreadyInitialised = false private var mService: VPNService = service + private var bytesInIndex = -1 + private var bytesOutIndex = -1 + + init { + findConfigIndicies() + } + + private fun findConfigIndicies() { + val n: Int = stats_n() + + for (i in 0 until n) { + val name: String = stats_name(i) + if (name == "BYTES_IN") bytesInIndex = i + if (name == "BYTES_OUT") bytesOutIndex = i + } + } + + fun getTotalRxBytes(): Long { + return stats_value(bytesInIndex) + } + + fun getTotalTxBytes(): Long { + return stats_value(bytesOutIndex) + } + override fun run() { val config: ClientAPI_Config = ClientAPI_Config() diff --git a/client/android/src/org/amnezia/vpn/VPNService.kt b/client/android/src/org/amnezia/vpn/VPNService.kt index 23e9ff0c5..6ca99e87d 100644 --- a/client/android/src/org/amnezia/vpn/VPNService.kt +++ b/client/android/src/org/amnezia/vpn/VPNService.kt @@ -41,6 +41,7 @@ import java.io.Closeable import java.io.File import java.io.FileDescriptor import java.io.IOException +import java.lang.Exception import android.net.VpnService as BaseVpnService @@ -153,31 +154,6 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface { private var flags = 0 private var startId = 0 - private lateinit var mMessenger: Messenger - - internal class ExternalConfigImportHandler( - context: Context, - private val serviceBinder: VPNServiceBinder, - private val applicationContext: Context = context.applicationContext - ) : Handler() { - - override fun handleMessage(msg: Message) { - when (msg.what) { - IMPORT_COMMAND_CODE -> { - val data = msg.data.getString(IMPORT_CONFIG_KEY) - - if (data != null) { - serviceBinder.importConfig(data) - } - } - - else -> { - super.handleMessage(msg) - } - } - } - } - fun init() { if (mAlreadyInitialised) { return @@ -216,13 +192,6 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface { override fun onBind(intent: Intent): IBinder { Log.v(tag, "Aman: onBind....................") - if (intent.action != null && intent.action == IMPORT_ACTION_CODE) { - Log.v(tag, "Service bind for import of config") - mMessenger = Messenger(ExternalConfigImportHandler(this, mBinder)) - return mMessenger.binder - } - - Log.v(tag, "Regular service bind") when (mProtocol) { "shadowsocks" -> { when (intent.action) { @@ -306,9 +275,16 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface { return mConnectionTime } - var isUp: Boolean + var isUp: Boolean = false get() { - return currentTunnelHandle >= 0 + return when (mProtocol) { + "openvpn" -> { + field + } + else -> { + currentTunnelHandle >= 0 + } + } } set(value) { if (value) { @@ -319,17 +295,52 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface { mBinder.dispatchEvent(VPNServiceBinder.EVENTS.disconnected, "") mConnectionTime = 0 } + val status: JSONObject get() { val deviceIpv4: String = "" + + val status = when (mProtocol) { + "openvpn" -> { + if (mOpenVPNThreadv3 == null) { + Status(null, null, null, null) + } else { + val rx = mOpenVPNThreadv3?.getTotalRxBytes() ?: "" + val tx = mOpenVPNThreadv3?.getTotalTxBytes() ?: "" + + Status( + rx.toString(), + tx.toString(), + if (mConfig!!.has("server")) { mConfig?.getJSONObject("server")?.getString("ipv4Gateway") } else {""}, + if (mConfig!!.has("device")) { mConfig?.getJSONObject("device")?.getString("ipv4Address") } else {""} + ) + } + } + else -> { + Status( + getConfigValue("rx_bytes"), + getConfigValue("tx_bytes"), + if (mConfig!!.has("server")) { mConfig?.getJSONObject("server")?.getString("ipv4Gateway") } else {""}, + if (mConfig!!.has("server")) {mConfig?.getJSONObject("device")?.getString("ipv4Address") } else {""} + ) + } + } + return JSONObject().apply { - putOpt("rx_bytes", getConfigValue("rx_bytes")) - putOpt("tx_bytes", getConfigValue("tx_bytes")) - putOpt("endpoint", mConfig?.getJSONObject("server")?.getString("ipv4Gateway")) - putOpt("deviceIpv4", mConfig?.getJSONObject("device")?.getString("ipv4Address")) + putOpt("rx_bytes", status.rxBytes) + putOpt("tx_bytes", status.txBytes) + putOpt("endpoint", status.endpoint) + putOpt("deviceIpv4", status.device) } } + data class Status( + var rxBytes: String?, + var txBytes: String?, + var endpoint: String?, + var device: String? + ) + /* * Checks if the VPN Permission is given. * If the permission is given, returns true @@ -677,6 +688,7 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface { private fun startOpenVpn() { mOpenVPNThreadv3 = OpenVPNThreadv3(this) + Thread({ mOpenVPNThreadv3?.run() }).start() diff --git a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt index 640a0657f..239269a5b 100644 --- a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt +++ b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt @@ -33,6 +33,7 @@ class VPNServiceBinder(service: VPNService) : Binder() { const val setNotificationText = 8 const val setFallBackNotification = 9 const val shareConfig = 10 + const val importConfig = 11 } /** @@ -75,13 +76,14 @@ class VPNServiceBinder(service: VPNService) : Binder() { ACTIONS.resumeActivate -> { // [data] is empty // Activate the current tunnel - try { - mResumeConfig?.let { this.mService.turnOn(it) } + Log.i(tag, "resume activate") + try { + mResumeConfig?.let { this.mService.turnOn(it) } } catch (e: Exception) { Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}") } return true - } + } ACTIONS.deactivate -> { // [data] here is empty @@ -90,6 +92,7 @@ class VPNServiceBinder(service: VPNService) : Binder() { } ACTIONS.registerEventListener -> { + Log.i(tag, "register: start") // [data] contains the Binder that we need to dispatch the Events val binder = data.readStrongBinder() mListener = binder @@ -150,6 +153,23 @@ class VPNServiceBinder(service: VPNService) : Binder() { return true } + ACTIONS.importConfig -> { + val buffer = data.readString() + + val obj = JSONObject() + obj.put("config", buffer) + + val resultString = obj.toString() + + Log.i(tag, "Transact import config request") + + if (mListener != null) { + dispatchEvent(EVENTS.configImport, resultString) + } else { + mImportedConfig = resultString + } + } + IBinder.LAST_CALL_TRANSACTION -> { Log.e(tag, "The OS Requested to shut down the VPN") this.mService.turnOff() @@ -176,9 +196,12 @@ 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) + } else { + Log.i(tag, "Dispatching event: binder NOT alive") } } } catch (e: DeadObjectException) { @@ -197,23 +220,7 @@ class VPNServiceBinder(service: VPNService) : Binder() { const val statisticUpdate = 3 const val backendLogs = 4 const val activationError = 5 - const val configImport = 6 - } - - fun importConfig(config: String) { - val obj = JSONObject() - obj.put("config", config) - - val resultString = obj.toString() - - Log.i(tag, "Transact import config request") - - if (mListener != null) { - Log.i(tag, "binder alive") - dispatchEvent(EVENTS.configImport, resultString) - } else { - Log.i(tag, "binder NOT alive") - mImportedConfig = resultString - } + const val permissionRequired = 6 + const val configImport = 7 } } diff --git a/client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt b/client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt index 7888f6d6d..24c33ffc3 100644 --- a/client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt +++ b/client/android/src/org/amnezia/vpn/qt/AmneziaApp.kt @@ -3,8 +3,8 @@ package org.amnezia.vpn.qt import android.content.res.Configuration import org.amnezia.vpn.shadowsocks.core.Core import org.amnezia.vpn.shadowsocks.core.VpnManager -import org.qtproject.qt5.android.bindings.QtActivity -import org.qtproject.qt5.android.bindings.QtApplication +import org.qtproject.qt.android.bindings.QtActivity +import org.qtproject.qt.android.bindings.QtApplication import android.app.Application class AmneziaApp: Application() { diff --git a/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt index c5b5107e1..673c4b593 100644 --- a/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt +++ b/client/android/src/org/amnezia/vpn/qt/VPNActivity.kt @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package org.amnezia.vpn.qt; import android.Manifest @@ -20,18 +24,33 @@ import org.amnezia.vpn.VPNServiceBinder import org.amnezia.vpn.IMPORT_COMMAND_CODE import org.amnezia.vpn.IMPORT_ACTION_CODE import org.amnezia.vpn.IMPORT_CONFIG_KEY -import org.qtproject.qt5.android.bindings.QtActivity +import org.qtproject.qt.android.bindings.QtActivity import java.io.* -class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { +class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() { private var configString: String? = null - private var vpnServiceBinder: Messenger? = null + private var vpnServiceBinder: IBinder? = null private var isBound = false private val TAG = "VPNActivity" private val STORAGE_PERMISSION_CODE = 42 + companion object { + private lateinit var instance: VPNActivity + + @JvmStatic fun getInstance(): VPNActivity { + return instance + } + + @JvmStatic fun connectService() { + VPNActivity.getInstance().initServiceConnection() + } + + @JvmStatic fun sendToService(actionCode: Int, body: String) { + VPNActivity.getInstance().dispatchParcel(actionCode, body) + } + } override fun onCreate(savedInstanceState: Bundle?) { val newIntent = intent @@ -42,6 +61,48 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { } super.onCreate(savedInstanceState) + + instance = this; + } + + 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. + // since we have no use of the clipboard in android 10+ + // we _can_ return null + // And we defnitly should since android 12 displays clipboard access. + null + } else { + super.getSystemService(name) + } + } + + external fun handleBackButton(): Boolean + + external fun onServiceMessage(actionCode: Int, body: String?) + external fun qtOnServiceConnected() + external fun qtOnServiceDisconnected() + + private fun dispatchParcel(actionCode: Int, body: String) { + if (!isBound) { + Log.d(TAG, "dispatchParcel: not bound") + return + } else { + Log.d(TAG, "dispatchParcel: bound") + } + + val out: Parcel = Parcel.obtain() + out.writeByteArray(body.toByteArray()) + + try { + vpnServiceBinder?.transact(actionCode, out, Parcel.obtain(), 0) + } catch (e: DeadObjectException) { + isBound = false + vpnServiceBinder = null + qtOnServiceDisconnected() + } catch (e: RemoteException) { + e.printStackTrace() + } } override fun onNewIntent(newIntent: Intent) { @@ -63,19 +124,11 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { override fun onResume() { super.onResume() - if (configString != null && !isBound) { - bindVpnService() + if (configString != null && isBound) { + sendImportConfigCommand() } } - override fun onPause() { - if (vpnServiceBinder != null && isBound) { - unbindService(connection) - isBound = false - } - super.onPause() - } - private fun isReadStorageAllowed(): Boolean { val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) return permissionStatus == PackageManager.PERMISSION_GRANTED @@ -90,8 +143,15 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { 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) { - bindVpnService() + 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() @@ -99,17 +159,6 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { } } - private fun bindVpnService() { - try { - val intent = Intent(this, VPNService::class.java) - intent.action = IMPORT_ACTION_CODE - - bindService(intent, connection, Context.BIND_AUTO_CREATE) - } catch (e: Exception) { - e.printStackTrace() - } - } - private fun processIntent(intent: Intent, action: String): String? { val scheme = intent.scheme @@ -158,23 +207,35 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { } } + private fun sendImportConfigCommand() { + if (configString != null) { + val msg: Parcel = Parcel.obtain() + msg.writeString(configString!!) + + try { + vpnServiceBinder?.transact(ACTION_IMPORT_CONFIG, msg, Parcel.obtain(), 0) + } catch (e: RemoteException) { + e.printStackTrace() + } + + configString = null + } + } + private var connection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { - vpnServiceBinder = Messenger(binder) + vpnServiceBinder = binder - if (configString != null) { - val msg: Message = Message.obtain(null, IMPORT_COMMAND_CODE, 0, 0) - val bundle = Bundle() - bundle.putString(IMPORT_CONFIG_KEY, configString!!) - msg.data = bundle - - try { - vpnServiceBinder?.send(msg) - } catch (e: RemoteException) { - e.printStackTrace() - } - - configString = null + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + if (registerBinder()){ + qtOnServiceConnected(); + } else { + qtOnServiceDisconnected(); + return } isBound = true @@ -183,9 +244,75 @@ class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() { override fun onServiceDisconnected(className: ComponentName) { vpnServiceBinder = null isBound = false + qtOnServiceDisconnected(); } } + private fun registerBinder(): Boolean { + val binder = VPNClientBinder() + val out: Parcel = Parcel.obtain() + out.writeStrongBinder(binder) + + try { + // Register our IBinder Listener + vpnServiceBinder?.transact(ACTION_REGISTER_LISTENER, out, Parcel.obtain(), 0) + return true + } catch (e: DeadObjectException) { + isBound = false + vpnServiceBinder = null + } catch (e: RemoteException) { + e.printStackTrace() + } + return false + } + + private fun initServiceConnection() { + // We already have a connection to the service, + // just need to re-register the binder + if (isBound && vpnServiceBinder!!.isBinderAlive() && registerBinder()) { + qtOnServiceConnected() + return + } + + bindService(Intent(this, VPNService::class.java), connection, Context.BIND_AUTO_CREATE) + } + + // TODO: Move all ipc codes into a shared lib. + // this is getting out of hand. + private val PERMISSION_TRANSACTION = 1337 + private val ACTION_REGISTER_LISTENER = 3 + private val ACTION_RESUME_ACTIVATE = 7 + private val ACTION_IMPORT_CONFIG = 11 + private val EVENT_PERMISSION_REQURED = 6 + private val EVENT_DISCONNECTED = 2 + + + fun onPermissionRequest(code: Int, data: Parcel?) { + if (code != EVENT_PERMISSION_REQURED) { + return + } + + val x = Intent() + x.readFromParcel(data) + + startActivityForResult(x, PERMISSION_TRANSACTION) + } + + override protected fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == PERMISSION_TRANSACTION) { + // THATS US! + if (resultCode == RESULT_OK) { + // Prompt accepted, tell service to retry. + dispatchParcel(ACTION_RESUME_ACTIVATE, "") + } else { + // Tell the Client we've disconnected + onServiceMessage(EVENT_DISCONNECTED, "") + } + return + } + super.onActivityResult(requestCode, resultCode, data) + } + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) { onBackPressed() diff --git a/client/android/src/org/amnezia/vpn/qt/VPNApplication.java b/client/android/src/org/amnezia/vpn/qt/VPNApplication.java index 295146339..639b5a1e9 100644 --- a/client/android/src/org/amnezia/vpn/qt/VPNApplication.java +++ b/client/android/src/org/amnezia/vpn/qt/VPNApplication.java @@ -5,7 +5,7 @@ import androidx.annotation.NonNull; import org.amnezia.vpn.shadowsocks.core.Core; import org.amnezia.vpn.shadowsocks.core.VpnManager; -public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication { +public class VPNApplication extends org.qtproject.qt.android.bindings.QtApplication { private static VPNApplication instance; @Override diff --git a/client/android/src/org/amnezia/vpn/qt/VPNClientBinder.kt b/client/android/src/org/amnezia/vpn/qt/VPNClientBinder.kt new file mode 100644 index 000000000..6a8fa086d --- /dev/null +++ b/client/android/src/org/amnezia/vpn/qt/VPNClientBinder.kt @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.amnezia.vpn.qt + +import android.os.Binder +import android.os.Parcel +import android.util.Log + +const val permissionRequired = 6 + +class VPNClientBinder() : Binder() { + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + if (code == permissionRequired) { + VPNActivity.getInstance().onPermissionRequest(code, data) + return true + } + + val buffer = data.createByteArray() + val stringData = buffer?.let { String(it) } + VPNActivity.getInstance().onServiceMessage(code, stringData) + + return true + } +} diff --git a/client/android/src/org/ftylitak/qzxing/QZXingLiveActivity.java b/client/android/src/org/ftylitak/qzxing/QZXingLiveActivity.java index 37ccb6ce3..b6a4649d3 100644 --- a/client/android/src/org/ftylitak/qzxing/QZXingLiveActivity.java +++ b/client/android/src/org/ftylitak/qzxing/QZXingLiveActivity.java @@ -2,7 +2,7 @@ package org.ftylitak.qzxing; import android.Manifest; import android.content.pm.PackageManager; -import org.qtproject.qt5.android.bindings.QtActivity; +import org.qtproject.qt.android.bindings.QtActivity; import static org.ftylitak.qzxing.Utilities.REQUEST_CAMERA; public class QZXingLiveActivity extends QtActivity { diff --git a/client/client.pro b/client/client.pro index 785005063..4810d0bab 100644 --- a/client/client.pro +++ b/client/client.pro @@ -1,4 +1,4 @@ -QT += widgets core gui network xml remoteobjects quick svg quickcontrols2 core5compat +QT += widgets core gui network xml remoteobjects quick svg quickcontrols2 equals(QT_MAJOR_VERSION, 6): QT += core5compat TARGET = AmneziaVPN @@ -241,14 +241,8 @@ android { versionAtLeast(QT_VERSION, 6.0.0) { # We need to include qtprivate api's # As QAndroidBinder is not yet implemented with a public api - QT+=core-private - ANDROID_ABIS=ANDROID_TARGET_ARCH - - # for not changing qtkeychain sources for qt6 - QT -= androidextras - } - else { - QT += androidextras + QT += core-private + ANDROID_ABIS = $$ANDROID_TARGET_ARCH } DEFINES += MVPN_ANDROID @@ -258,13 +252,16 @@ android { HEADERS += \ platforms/android/android_controller.h \ platforms/android/android_notificationhandler.h \ - protocols/android_vpnprotocol.h + protocols/android_vpnprotocol.h \ + platforms/android/androidutils.h \ + platforms/android/androidvpnactivity.h SOURCES += \ platforms/android/android_controller.cpp \ platforms/android/android_notificationhandler.cpp \ - protocols/android_vpnprotocol.cpp - + protocols/android_vpnprotocol.cpp \ + platforms/android/androidutils.cpp \ + platforms/android/androidvpnactivity.cpp DISTFILES += \ android/AndroidManifest.xml \ @@ -293,6 +290,7 @@ android { ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android for (abi, ANDROID_ABIS): { + equals(ANDROID_TARGET_ARCH,$$abi) { LIBS += $$PWD/3rd/OpenSSL/lib/android/$${abi}/libcrypto.a LIBS += $$PWD/3rd/OpenSSL/lib/android/$${abi}/libssl.a diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 2fcfa9d1f..a87b4df57 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -19,8 +19,8 @@ add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain) set(LIBS ${LIBS} qt6keychain) include_directories( - ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/include ${CLIENT_ROOT_DIR}/3rd/OpenSSL/include + ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/include ${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain ) diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 12589664b..b25fce78f 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + #include #include #include @@ -7,51 +11,131 @@ #include #include -#include - #include "android_controller.h" -#include "core/errorstrings.h" +#include "private/qandroidextras_p.h" #include "ui/pages_logic/StartPageLogic.h" -// Binder Codes for VPNServiceBinder -// See also - VPNServiceBinder.kt -// Actions that are Requestable -const int ACTION_ACTIVATE = 1; -const int ACTION_DEACTIVATE = 2; -const int ACTION_REGISTER_LISTENER = 3; -const int ACTION_REQUEST_STATISTIC = 4; -const int ACTION_REQUEST_GET_LOG = 5; -const int ACTION_REQUEST_CLEANUP_LOG = 6; -const int ACTION_RESUME_ACTIVATE = 7; -const int ACTION_SET_NOTIFICATION_TEXT = 8; -const int ACTION_SET_NOTIFICATION_FALLBACK = 9; -const int ACTION_SHARE_CONFIG = 10; - -// Event Types that will be Dispatched after registration -const int EVENT_INIT = 0; -const int EVENT_CONNECTED = 1; -const int EVENT_DISCONNECTED = 2; -const int EVENT_STATISTIC_UPDATE = 3; -const int EVENT_BACKEND_LOGS = 4; -const int EVENT_ACTIVATION_ERROR = 5; -const int EVENT_CONFIG_IMPORT = 6; +#include "androidvpnactivity.h" +#include "androidutils.h" namespace { AndroidController* s_instance = nullptr; constexpr auto PERMISSIONHELPER_CLASS = "org/amnezia/vpn/qt/VPNPermissionHelper"; - } // namespace -AndroidController::AndroidController(): - m_binder(this) +AndroidController::AndroidController() : QObject() { + connect(this, &AndroidController::scheduleStatusCheckSignal, this, &AndroidController::scheduleStatusCheckSlot); + s_instance = this; + + auto activity = AndroidVPNActivity::instance(); + + connect(activity, &AndroidVPNActivity::serviceConnected, this, []() { + qDebug() << "Transact: service connected"; + AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_STATISTIC, ""); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventInitialized, this, + [this](const QString& parcelBody) { + // We might get multiple Init events as widgets, or fragments + // might query this. + if (m_init) { + return; + } + + qDebug() << "Transact: init"; + + m_init = true; + + auto doc = QJsonDocument::fromJson(parcelBody.toUtf8()); + qlonglong time = doc.object()["time"].toVariant().toLongLong(); + + isConnected = doc.object()["connected"].toBool(); + + emit initialized( + true, isConnected, + time > 0 ? QDateTime::fromMSecsSinceEpoch(time) : QDateTime()); + + setFallbackConnectedNotification(); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventConnected, this, + [this](const QString& parcelBody) { + Q_UNUSED(parcelBody); + qDebug() << "Transact: connected"; + + isConnected = true; + + emit scheduleStatusCheckSignal(); + + emit connectionStateChanged(VpnProtocol::Connected); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventDisconnected, this, + [this]() { + qDebug() << "Transact: disconnected"; + + isConnected = false; + + emit connectionStateChanged(VpnProtocol::Disconnected); + }, Qt::QueuedConnection); + + 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(); + QString tx = doc.object()["tx_bytes"].toString(); + QString endpoint = doc.object()["endpoint"].toString(); + QString deviceIPv4 = doc.object()["deviceIpv4"].toString(); + + emit statusUpdated(rx, tx, endpoint, deviceIPv4); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventBackendLogs, this, + [this](const QString& parcelBody) { + qDebug() << "Transact: backend logs"; + + QString buffer = parcelBody.toUtf8(); + if (m_logCallback) { + m_logCallback(buffer); + } + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventActivationError, this, + [this](const QString& parcelBody) { + Q_UNUSED(parcelBody) + qDebug() << "Transact: error"; + emit connectionStateChanged(VpnProtocol::Error); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::eventConfigImport, this, + [this](const QString& parcelBody) { + qDebug() << "Transact: config import"; + auto doc = QJsonDocument::fromJson(parcelBody.toUtf8()); + + QString buffer = doc.object()["config"].toString(); + qDebug() << "Transact: config string" << buffer; + importConfig(buffer); + }, Qt::QueuedConnection); + + connect(activity, &AndroidVPNActivity::serviceDisconnected, this, + [this]() { + qDebug() << "Transact: service disconnected"; + m_serviceConnected = false; + }, Qt::QueuedConnection); } AndroidController* AndroidController::instance() { - if (!s_instance) s_instance = new AndroidController(); + if (!s_instance) { + s_instance = new AndroidController(); + } + return s_instance; } @@ -65,71 +149,43 @@ bool AndroidController::initialize(StartPageLogic *startPageLogic) JNINativeMethod methods[]{{"startActivityForResult", "(Landroid/content/Intent;)V", reinterpret_cast(startActivityForResult)}}; - QAndroidJniObject javaClass(PERMISSIONHELPER_CLASS); - QAndroidJniEnvironment env; + QJniObject javaClass(PERMISSIONHELPER_CLASS); + QJniEnvironment env; jclass objectClass = env->GetObjectClass(javaClass.object()); - env->RegisterNatives(objectClass, methods, - sizeof(methods) / sizeof(methods[0])); + env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0])); env->DeleteLocalRef(objectClass); - auto appContext = QtAndroid::androidActivity().callObjectMethod( - "getApplicationContext", "()Landroid/content/Context;"); + AndroidVPNActivity::connectService(); - QAndroidJniObject::callStaticMethod( - "org/amnezia/vpn/VPNService", "startService", - "(Landroid/content/Context;)V", appContext.object()); - - // Start the VPN Service (if not yet) and Bind to it - const bool bindResult = QtAndroid::bindService( - QAndroidIntent(appContext.object(), "org.amnezia.vpn.VPNService"), - *this, QtAndroid::BindFlag::AutoCreate); - qDebug() << "Binding to the service..." << bindResult; - - return bindResult; + return true; } ErrorCode AndroidController::start() { - - //qDebug().noquote() << "AndroidController::start" << QJsonDocument(m_rawConfig).toJson(); qDebug() << "Prompting for VPN permission"; - auto appContext = QtAndroid::androidActivity().callObjectMethod( + QJniObject activity = AndroidUtils::getActivity(); + auto appContext = activity.callObjectMethod( "getApplicationContext", "()Landroid/content/Context;"); - QAndroidJniObject::callStaticMethod( + QJniObject::callStaticMethod( PERMISSIONHELPER_CLASS, "startService", "(Landroid/content/Context;)V", appContext.object()); - QAndroidParcel sendData; - sendData.writeData(QJsonDocument(m_vpnConfig).toJson()); - bool activateResult = false; - while (!activateResult){ - activateResult = m_serviceBinder.transact(ACTION_ACTIVATE, sendData, nullptr); - } + QJsonDocument doc(m_vpnConfig); + AndroidVPNActivity::sendToService(ServiceAction::ACTION_ACTIVATE, doc.toJson()); - return activateResult ? NoError : UnknownError; + return NoError; } void AndroidController::stop() { qDebug() << "AndroidController::stop"; -// if (reason != ReasonNone) { -// // Just show that we're disconnected -// // we're doing the actual disconnect once -// // the vpn-service has the new server ready in Action->Activate -// emit disconnected(); -// qCritical() << "deactivation skipped for Switching"; -// return; -// } - - QAndroidParcel nullData; - m_serviceBinder.transact(ACTION_DEACTIVATE, nullData, nullptr); + AndroidVPNActivity::sendToService(ServiceAction::ACTION_DEACTIVATE, QString()); } // Activates the tunnel that is currently set // in the VPN Service void AndroidController::resumeStart() { - QAndroidParcel nullData; - m_serviceBinder.transact(ACTION_RESUME_ACTIVATE, nullData, nullptr); + AndroidVPNActivity::sendToService(ServiceAction::ACTION_RESUME_ACTIVATE, QString()); } /* @@ -138,14 +194,13 @@ void AndroidController::resumeStart() { void AndroidController::setNotificationText(const QString& title, const QString& message, int timerSec) { - QJsonObject args; - args["title"] = title; - args["message"] = message; - args["sec"] = timerSec; - QJsonDocument doc(args); - QAndroidParcel data; - data.writeData(doc.toJson()); - m_serviceBinder.transact(ACTION_SET_NOTIFICATION_TEXT, data, nullptr); + QJsonObject args; + args["title"] = title; + args["message"] = message; + args["sec"] = timerSec; + QJsonDocument doc(args); + + AndroidVPNActivity::sendToService(ServiceAction::ACTION_SET_NOTIFICATION_TEXT, doc.toJson()); } void AndroidController::shareConfig(const QString& configContent, const QString& suggestedName) { @@ -153,9 +208,8 @@ void AndroidController::shareConfig(const QString& configContent, const QString& rootObject["data"] = configContent; rootObject["suggestedName"] = suggestedName; QJsonDocument doc(rootObject); - QAndroidParcel parcel; - parcel.writeData(doc.toJson()); - m_serviceBinder.transact(ACTION_SHARE_CONFIG, parcel, nullptr); + + AndroidVPNActivity::sendToService(ServiceAction::ACTION_SHARE_CONFIG, doc.toJson()); } /* @@ -164,64 +218,40 @@ void AndroidController::shareConfig(const QString& configContent, const QString& * e.g via always-on vpn */ void AndroidController::setFallbackConnectedNotification() { - QJsonObject args; - args["title"] = tr("AmneziaVPN"); - //% "Ready for you to connect" - //: Refers to the app - which is currently running the background and waiting - args["message"] = tr("VPN Connected"); - QJsonDocument doc(args); - QAndroidParcel data; - data.writeData(doc.toJson()); - m_serviceBinder.transact(ACTION_SET_NOTIFICATION_FALLBACK, data, nullptr); + QJsonObject args; + args["title"] = tr("AmneziaVPN"); + //% "Ready for you to connect" + //: Refers to the app - which is currently running the background and waiting + args["message"] = tr("VPN Connected"); + QJsonDocument doc(args); + + AndroidVPNActivity::sendToService(ServiceAction::ACTION_SET_NOTIFICATION_FALLBACK, doc.toJson()); } void AndroidController::checkStatus() { - qDebug() << "check status"; + qDebug() << "check status"; - QAndroidParcel nullParcel; - m_serviceBinder.transact(ACTION_REQUEST_STATISTIC, nullParcel, nullptr); + AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_STATISTIC, QString()); } void AndroidController::getBackendLogs(std::function&& a_callback) { - qDebug() << "get logs"; + qDebug() << "get logs"; - m_logCallback = std::move(a_callback); - QAndroidParcel nullData, replyData; - m_serviceBinder.transact(ACTION_REQUEST_GET_LOG, nullData, &replyData); + m_logCallback = std::move(a_callback); + + AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_GET_LOG, QString()); } void AndroidController::cleanupBackendLogs() { - qDebug() << "cleanup logs"; + qDebug() << "cleanup logs"; - QAndroidParcel nullParcel; - m_serviceBinder.transact(ACTION_REQUEST_CLEANUP_LOG, nullParcel, nullptr); + AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_CLEANUP_LOG, QString()); } void AndroidController::importConfig(const QString& data){ m_startPageLogic->importConnectionFromCode(data); } -void AndroidController::onServiceConnected( - const QString& name, const QAndroidBinder& serviceBinder) { - qDebug() << "Server " + name + " connected"; - - Q_UNUSED(name); - - m_serviceBinder = serviceBinder; - - // Send the Service our Binder to recive incoming Events - QAndroidParcel binderParcel; - binderParcel.writeBinder(m_binder); - m_serviceBinder.transact(ACTION_REGISTER_LISTENER, binderParcel, nullptr); -} - -void AndroidController::onServiceDisconnected(const QString& name) { - qDebug() << "Server disconnected"; - m_serviceConnected = false; - Q_UNUSED(name); - // TODO: Maybe restart? Or crash? -} - const QJsonObject &AndroidController::vpnConfig() const { return m_vpnConfig; @@ -232,86 +262,14 @@ void AndroidController::setVpnConfig(const QJsonObject &newVpnConfig) m_vpnConfig = newVpnConfig; } - -/** - * @brief AndroidController::VPNBinder::onTransact - * @param code the Event-Type we get From the VPNService See - * @param data - Might contain UTF-8 JSON in case the Event has a payload - * @param reply - always null - * @param flags - unused - * @return Returns true is the code was a valid Event Code - */ -bool AndroidController::VPNBinder::onTransact(int code, - const QAndroidParcel& data, - const QAndroidParcel& reply, - QAndroidBinder::CallType flags) { - Q_UNUSED(data); - Q_UNUSED(reply); - Q_UNUSED(flags); - - QJsonDocument doc; - QString buffer; - switch (code) { - case EVENT_INIT: - qDebug() << "Transact: init"; - doc = QJsonDocument::fromJson(data.readData()); - emit m_controller->initialized( - true, doc.object()["connected"].toBool(), - QDateTime::fromMSecsSinceEpoch( - doc.object()["time"].toVariant().toLongLong())); - // Pass a localised version of the Fallback string for the Notification - m_controller->setFallbackConnectedNotification(); - - break; - case EVENT_CONNECTED: - qDebug() << "Transact: connected"; - emit m_controller->connectionStateChanged(VpnProtocol::Connected); - break; - case EVENT_DISCONNECTED: - qDebug() << "Transact: disconnected"; - emit m_controller->connectionStateChanged(VpnProtocol::Disconnected); - break; - case EVENT_STATISTIC_UPDATE: - qDebug() << "Transact:: update"; - - // Data is here a JSON String - doc = QJsonDocument::fromJson(data.readData()); - // TODO update counters -// emit m_controller->statusUpdated(doc.object()["endpoint"].toString(), -// doc.object()["deviceIpv4"].toString(), -// doc.object()["totalTX"].toInt(), -// doc.object()["totalRX"].toInt()); - break; - case EVENT_BACKEND_LOGS: - qDebug() << "Transact: backend logs"; - - buffer = readUTF8Parcel(data); - if (m_controller->m_logCallback) { - m_controller->m_logCallback(buffer); - } - break; - case EVENT_ACTIVATION_ERROR: - qDebug() << "Transact: error"; - emit m_controller->connectionStateChanged(VpnProtocol::Error); - break; - case EVENT_CONFIG_IMPORT: - qDebug() << "Transact: config import"; - doc = QJsonDocument::fromJson(data.readData()); - buffer = doc.object()["config"].toString(); - qDebug() << "Transact: config string" << buffer; - m_controller->importConfig(buffer); - break; - default: - qWarning() << "Transact: Invalid!"; - break; - } - - return true; -} - -QString AndroidController::VPNBinder::readUTF8Parcel(QAndroidParcel data) { - // 106 is the Code for UTF-8 - return QTextCodec::codecForMib(106)->toUnicode(data.readData()); +void AndroidController::scheduleStatusCheckSlot() +{ + QTimer::singleShot(1000, [this]() { + if (isConnected) { + checkStatus(); + emit scheduleStatusCheckSignal(); + } + }); } const int ACTIVITY_RESULT_OK = 0xffffffff; @@ -322,34 +280,34 @@ const int ACTIVITY_RESULT_OK = 0xffffffff; */ void AndroidController::startActivityForResult(JNIEnv *env, jobject, jobject intent) { - qDebug() << "start activity"; + qDebug() << "start vpnPermissionHelper"; Q_UNUSED(env); - QtAndroid::startActivity(intent, 1337, - [](int receiverRequestCode, int resultCode, - const QAndroidJniObject& data) { - // Currently this function just used in - // VPNService.kt::checkPersmissions. So the result - // we're getting is if the User gave us the - // Vpn.bind permission. In case of NO we should - // abort. - Q_UNUSED(receiverRequestCode); - Q_UNUSED(data); - AndroidController* controller = - AndroidController::instance(); - if (!controller) { - return; - } + QtAndroidPrivate::startActivity(intent, 1337, + [](int receiverRequestCode, int resultCode, + const QJniObject& data) { + // Currently this function just used in + // VPNService.kt::checkPersmissions. So the result + // we're getting is if the User gave us the + // Vpn.bind permission. In case of NO we should + // abort. + Q_UNUSED(receiverRequestCode); + Q_UNUSED(data); - if (resultCode == ACTIVITY_RESULT_OK) { - qDebug() << "VPN PROMPT RESULT - Accepted"; - controller->resumeStart(); - return; - } - // If the request got rejected abort the current - // connection. - qWarning() << "VPN PROMPT RESULT - Rejected"; - emit controller->connectionStateChanged(VpnProtocol::Disconnected); - }); + AndroidController* controller = AndroidController::instance(); + if (!controller) { + return; + } + + if (resultCode == ACTIVITY_RESULT_OK) { + qDebug() << "VPN PROMPT RESULT - Accepted"; + controller->resumeStart(); + return; + } + // If the request got rejected abort the current + // connection. + qWarning() << "VPN PROMPT RESULT - Rejected"; + emit controller->connectionStateChanged(VpnProtocol::Disconnected); + }); return; } diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index b2543d5dd..ddaa0d79f 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -1,16 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + #ifndef ANDROID_CONTROLLER_H #define ANDROID_CONTROLLER_H -#include +#include +#include -#include "ui/uilogic.h" #include "ui/pages_logic/StartPageLogic.h" #include "protocols/vpnprotocol.h" + using namespace amnezia; -class AndroidController : public QObject, public QAndroidServiceConnection +class AndroidController : public QObject { Q_OBJECT @@ -34,10 +39,6 @@ public: void cleanupBackendLogs(); void importConfig(const QString& data); - // from QAndroidServiceConnection - void onServiceConnected(const QString& name, const QAndroidBinder& serviceBinder) override; - void onServiceDisconnected(const QString& name) override; - const QJsonObject &vpnConfig() const; void setVpnConfig(const QJsonObject &newVpnConfig); @@ -52,13 +53,18 @@ signals: void initialized(bool status, bool connected, const QDateTime& connectionDate); + void statusUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4); + void scheduleStatusCheckSignal(); + protected slots: + void scheduleStatusCheckSlot(); protected: private: - //Protocol m_protocol; + bool m_init = false; + QJsonObject m_vpnConfig; StartPageLogic *m_startPageLogic; @@ -66,24 +72,11 @@ private: bool m_serviceConnected = false; std::function m_logCallback; - QAndroidBinder m_serviceBinder; - class VPNBinder : public QAndroidBinder { - public: - VPNBinder(AndroidController* controller) : m_controller(controller) {} - - bool onTransact(int code, const QAndroidParcel& data, - const QAndroidParcel& reply, - QAndroidBinder::CallType flags) override; - - QString readUTF8Parcel(QAndroidParcel data); - - private: - AndroidController* m_controller = nullptr; - }; - - VPNBinder m_binder; - static void startActivityForResult(JNIEnv* env, jobject /*thiz*/, jobject intent); + + bool isConnected = false; + + void scheduleStatusCheck(); }; #endif // ANDROID_CONTROLLER_H diff --git a/client/platforms/android/androidutils.cpp b/client/platforms/android/androidutils.cpp new file mode 100644 index 000000000..5e9f094cf --- /dev/null +++ b/client/platforms/android/androidutils.cpp @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "androidutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jni.h" + +namespace { + AndroidUtils* s_instance = nullptr; +} // namespace + +// static +QString AndroidUtils::GetDeviceName() { + QJniEnvironment env; + jclass BUILD = env->FindClass("android/os/Build"); + jfieldID model = env->GetStaticFieldID(BUILD, "MODEL", "Ljava/lang/String;"); + jstring value = (jstring)env->GetStaticObjectField(BUILD, model); + + if (!value) { + return QString("Android Device"); + } + + const char* buffer = env->GetStringUTFChars(value, nullptr); + if (!buffer) { + return QString("Android Device"); + } + + QString res(buffer); + env->ReleaseStringUTFChars(value, buffer); + + return res; +}; + +// static +AndroidUtils* AndroidUtils::instance() { + if (!s_instance) { + Q_ASSERT(qApp); + s_instance = new AndroidUtils(qApp); + } + + return s_instance; +} + +AndroidUtils::AndroidUtils(QObject* parent) : QObject(parent) { + Q_ASSERT(!s_instance); + s_instance = this; +} + +AndroidUtils::~AndroidUtils() { + Q_ASSERT(s_instance == this); + s_instance = nullptr; +} + +// static +void AndroidUtils::dispatchToMainThread(std::function callback) { + QTimer* timer = new QTimer(); + timer->moveToThread(qApp->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() { + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection); +} + +// static +QByteArray AndroidUtils::getQByteArrayFromJString(JNIEnv* env, jstring data) { + const char* buffer = env->GetStringUTFChars(data, nullptr); + if (!buffer) { + qDebug() << "getQByteArrayFromJString - failed to parse data."; + return QByteArray(); + } + + QByteArray out(buffer); + env->ReleaseStringUTFChars(data, buffer); + return out; +} + +// static +QString AndroidUtils::getQStringFromJString(JNIEnv* env, jstring data) { + const char* buffer = env->GetStringUTFChars(data, nullptr); + if (!buffer) { + qDebug() << "getQStringFromJString - failed to parse data."; + return QString(); + } + + QString out(buffer); + env->ReleaseStringUTFChars(data, buffer); + return out; +} + +// static +QJsonObject AndroidUtils::getQJsonObjectFromJString(JNIEnv* env, jstring data) { + QByteArray raw(getQByteArrayFromJString(env, data)); + QJsonParseError jsonError; + QJsonDocument json = QJsonDocument::fromJson(raw, &jsonError); + if (QJsonParseError::NoError != jsonError.error) { + qDebug() << "getQJsonObjectFromJstring - error parsing json. Code: " + << jsonError.error << "Offset: " << jsonError.offset + << "Message: " << jsonError.errorString() + << "Data: " << raw; + return QJsonObject(); + } + + if (!json.isObject()) { + qDebug() << "getQJsonObjectFromJString - object expected."; + return QJsonObject(); + } + + return json.object(); +} + +QJniObject AndroidUtils::getActivity() { + return QNativeInterface::QAndroidApplication::context(); +} + +int AndroidUtils::GetSDKVersion() { + QJniEnvironment env; + jclass versionClass = env->FindClass("android/os/Build$VERSION"); + jfieldID sdkIntFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I"); + int sdk = env->GetStaticIntField(versionClass, sdkIntFieldID); + + return sdk; +} + +QString AndroidUtils::GetManufacturer() { + QJniEnvironment env; + jclass buildClass = env->FindClass("android/os/Build"); + jfieldID manuFacturerField = + env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;"); + jstring value = + (jstring)env->GetStaticObjectField(buildClass, manuFacturerField); + + const char* buffer = env->GetStringUTFChars(value, nullptr); + + if (!buffer) { + qDebug() << "Failed to fetch MANUFACTURER"; + return QByteArray(); + } + + QString res(buffer); + qDebug() << "MANUFACTURER: " << res; + env->ReleaseStringUTFChars(value, buffer); + return res; +} + +void AndroidUtils::runOnAndroidThreadSync(const std::function runnable) { + QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable) + .waitForFinished(); +} + +void AndroidUtils::runOnAndroidThreadAsync(const std::function runnable) { + QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable); +} + +// Static +// Creates a copy of the passed QByteArray in the JVM and passes back a ref +jbyteArray AndroidUtils::tojByteArray(const QByteArray& data) { + QJniEnvironment env; + jbyteArray out = env->NewByteArray(data.size()); + env->SetByteArrayRegion(out, 0, data.size(), + reinterpret_cast(data.constData())); + return out; +} diff --git a/client/platforms/android/androidutils.h b/client/platforms/android/androidutils.h new file mode 100644 index 000000000..8559400c8 --- /dev/null +++ b/client/platforms/android/androidutils.h @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ANDROIDUTILS_H +#define ANDROIDUTILS_H + +#include + +#include +#include +#include +#include +#include + +class AndroidUtils final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(AndroidUtils) + +public: + static QString GetDeviceName(); + + static int GetSDKVersion(); + static QString GetManufacturer(); + + static AndroidUtils* instance(); + + static void dispatchToMainThread(std::function callback); + + static QByteArray getQByteArrayFromJString(JNIEnv* env, jstring data); + + static jbyteArray tojByteArray(const QByteArray& data); + + static QString getQStringFromJString(JNIEnv* env, jstring data); + + static QJsonObject getQJsonObjectFromJString(JNIEnv* env, jstring data); + + static QJniObject getActivity(); + + static void runOnAndroidThreadSync(const std::function runnable); + static void runOnAndroidThreadAsync(const std::function runnable); + +private: + AndroidUtils(QObject* parent); + ~AndroidUtils(); +}; + +#endif // ANDROIDUTILS_H diff --git a/client/platforms/android/androidvpnactivity.cpp b/client/platforms/android/androidvpnactivity.cpp new file mode 100644 index 000000000..0e8b9574e --- /dev/null +++ b/client/platforms/android/androidvpnactivity.cpp @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "androidvpnactivity.h" + +#include +#include +#include +#include +#include + +#include "androidutils.h" +#include "jni.h" + +namespace { + AndroidVPNActivity* s_instance = nullptr; + constexpr auto CLASSNAME = "org.amnezia.vpn.qt.VPNActivity"; +} + +AndroidVPNActivity::AndroidVPNActivity() { + AndroidUtils::runOnAndroidThreadAsync([]() { + JNINativeMethod methods[]{ + {"handleBackButton", "()Z", reinterpret_cast(handleBackButton)}, + {"onServiceMessage", "(ILjava/lang/String;)V", + reinterpret_cast(onServiceMessage)}, + {"qtOnServiceConnected", "()V", + reinterpret_cast(onServiceConnected)}, + {"qtOnServiceDisconnected", "()V", + reinterpret_cast(onServiceDisconnected)}, + }; + + QJniObject javaClass(CLASSNAME); + QJniEnvironment env; + jclass objectClass = env->GetObjectClass(javaClass.object()); + env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0])); + env->DeleteLocalRef(objectClass); + }); +} + +void AndroidVPNActivity::maybeInit() { + if (s_instance == nullptr) { + s_instance = new AndroidVPNActivity(); + } +} + +// static +bool AndroidVPNActivity::handleBackButton(JNIEnv* env, jobject thiz) { + Q_UNUSED(env); + Q_UNUSED(thiz); +} + +void AndroidVPNActivity::connectService() { + QJniObject::callStaticMethod(CLASSNAME, "connectService", "()V"); +} + +// static +AndroidVPNActivity* AndroidVPNActivity::instance() { + if (s_instance == nullptr) { + AndroidVPNActivity::maybeInit(); + } + + return s_instance; +} + +// static +void AndroidVPNActivity::sendToService(ServiceAction type, const QString& data) { + int messageType = (int)type; + + QJniEnvironment env; + QJniObject::callStaticMethod( + CLASSNAME, "sendToService", "(ILjava/lang/String;)V", + static_cast(messageType), + QJniObject::fromString(data).object()); +} + +// static +void AndroidVPNActivity::onServiceMessage(JNIEnv* env, jobject thiz, + jint messageType, jstring body) { + Q_UNUSED(thiz); + const char* buffer = env->GetStringUTFChars(body, nullptr); + if (!buffer) { + return; + } + + QString parcelBody(buffer); + env->ReleaseStringUTFChars(body, buffer); + AndroidUtils::dispatchToMainThread([messageType, parcelBody] { + AndroidVPNActivity::instance()->handleServiceMessage(messageType, + parcelBody); + }); +} + +void AndroidVPNActivity::handleServiceMessage(int code, const QString& data) { + auto mode = (ServiceEvents)code; + + switch (mode) { + case ServiceEvents::EVENT_INIT: + emit eventInitialized(data); + break; + case ServiceEvents::EVENT_CONNECTED: + emit eventConnected(data); + break; + case ServiceEvents::EVENT_DISCONNECTED: + emit eventDisconnected(data); + break; + case ServiceEvents::EVENT_STATISTIC_UPDATE: + emit eventStatisticUpdate(data); + break; + case ServiceEvents::EVENT_BACKEND_LOGS: + emit eventBackendLogs(data); + break; + case ServiceEvents::EVENT_ACTIVATION_ERROR: + emit eventActivationError(data); + break; + case ServiceEvents::EVENT_CONFIG_IMPORT: + emit eventConfigImport(data); + break; + default: + Q_ASSERT(false); + } +} + +void AndroidVPNActivity::onServiceConnected(JNIEnv* env, jobject thiz) { + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidVPNActivity::instance()->serviceConnected(); +} + +void AndroidVPNActivity::onServiceDisconnected(JNIEnv* env, jobject thiz) { + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidVPNActivity::instance()->serviceDisconnected(); +} diff --git a/client/platforms/android/androidvpnactivity.h b/client/platforms/android/androidvpnactivity.h new file mode 100644 index 000000000..49d1aae5e --- /dev/null +++ b/client/platforms/android/androidvpnactivity.h @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ANDROIDVPNACTIVITY_H +#define ANDROIDVPNACTIVITY_H + +#include + +#include "jni.h" + +// Binder Codes for VPNServiceBinder +// See also - VPNServiceBinder.kt +// Actions that are Requestable +enum ServiceAction { + // Activate the vpn. Body requires a json wg-conf + ACTION_ACTIVATE = 1, + // Deactivate the vpn. Body is empty + ACTION_DEACTIVATE = 2, + // Register an IBinder to recieve events body is an Ibinder + ACTION_REGISTERLISTENER = 3, + // Requests an EVENT_STATISTIC_UPDATE to be send + ACTION_REQUEST_STATISTIC = 4, + ACTION_REQUEST_GET_LOG = 5, + // Requests to clean up the internal log + ACTION_REQUEST_CLEANUP_LOG = 6, + // Retry activation using the last config + // Used when the activation is aborted for VPN-Permission prompt + ACTION_RESUME_ACTIVATE = 7, + // Sets the current notification text. + // Does nothing if there is no notification + ACTION_SET_NOTIFICATION_TEXT = 8, + // Sets the fallback text if the OS triggered the VPN-Service + // to show a notification + ACTION_SET_NOTIFICATION_FALLBACK = 9, + // Share used config + ACTION_SHARE_CONFIG = 10, +}; +typedef enum ServiceAction ServiceAction; + +// Event Types that will be Dispatched after registration +enum ServiceEvents { + // The Service has Accecpted our Binder + // Responds with the current status of the vpn. + EVENT_INIT = 0, + // WG-Go has enabled the adapter (empty response) + EVENT_CONNECTED = 1, + // WG-Go has disabled the adapter (empty response) + EVENT_DISCONNECTED = 2, + // Contains the Current transfered bytes to endpoint x. + EVENT_STATISTIC_UPDATE = 3, + EVENT_BACKEND_LOGS = 4, + // An Error happened during activation + // Contains the error message + EVENT_ACTIVATION_ERROR = 5, + EVENT_NEED_PERMISSION = 6, + // Import of existing config + EVENT_CONFIG_IMPORT = 7, +}; +typedef enum ServiceEvents ServiceEvents; + +class AndroidVPNActivity : public QObject +{ + Q_OBJECT + +public: + static void maybeInit(); + static AndroidVPNActivity* instance(); + static bool handleBackButton(JNIEnv* env, jobject thiz); + static void sendToService(ServiceAction type, const QString& data); + static void connectService(); + +signals: + void serviceConnected(); + void serviceDisconnected(); + void eventInitialized(const QString& data); + void eventConnected(const QString& data); + void eventDisconnected(const QString& data); + void eventStatisticUpdate(const QString& data); + void eventBackendLogs(const QString& data); + void eventActivationError(const QString& data); + void eventConfigImport(const QString& data); + +private: + AndroidVPNActivity(); + + static void onServiceMessage(JNIEnv* env, jobject thiz, jint messageType, jstring body); + static void onServiceConnected(JNIEnv* env, jobject thiz); + static void onServiceDisconnected(JNIEnv* env, jobject thiz); + void handleServiceMessage(int code, const QString& data); +}; + +#endif // ANDROIDVPNACTIVITY_H diff --git a/client/protocols/android_vpnprotocol.cpp b/client/protocols/android_vpnprotocol.cpp index d86a33424..521b08f2e 100644 --- a/client/protocols/android_vpnprotocol.cpp +++ b/client/protocols/android_vpnprotocol.cpp @@ -7,11 +7,7 @@ #include #include -#include - - #include "android_vpnprotocol.h" -#include "core/errorstrings.h" #include "platforms/android/android_controller.h" @@ -19,9 +15,7 @@ AndroidVpnProtocol::AndroidVpnProtocol(Proto protocol, const QJsonObject &configuration, QObject* parent) : VpnProtocol(configuration, parent), m_protocol(protocol) -{ - -} +{ } ErrorCode AndroidVpnProtocol::start() { @@ -35,3 +29,11 @@ void AndroidVpnProtocol::stop() AndroidController::instance()->stop(); } +void AndroidVpnProtocol::connectionDataUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4) +{ + quint64 rxBytes = totalRx.toLongLong(); + quint64 txBytes = totalTx.toLongLong(); + + setBytesChanged(rxBytes, txBytes); +} + diff --git a/client/protocols/android_vpnprotocol.h b/client/protocols/android_vpnprotocol.h index cdcff2c6b..ea87679a6 100644 --- a/client/protocols/android_vpnprotocol.h +++ b/client/protocols/android_vpnprotocol.h @@ -1,16 +1,11 @@ #ifndef ANDROID_VPNPROTOCOL_H #define ANDROID_VPNPROTOCOL_H -#include -#include - #include "vpnprotocol.h" #include "protocols/protocols_defs.h" using namespace amnezia; - - class AndroidVpnProtocol : public VpnProtocol { Q_OBJECT @@ -25,6 +20,9 @@ public: signals: +public slots: + void connectionDataUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4); + protected slots: protected: @@ -32,7 +30,6 @@ protected: private: Proto m_protocol; - }; #endif // ANDROID_VPNPROTOCOL_H diff --git a/client/protocols/ios_vpnprotocol.h b/client/protocols/ios_vpnprotocol.h index 7238fc36e..7f55be198 100644 --- a/client/protocols/ios_vpnprotocol.h +++ b/client/protocols/ios_vpnprotocol.h @@ -36,6 +36,7 @@ public: void cleanupBackendLogs(); signals: + void newTransmitedDataCount(quint64 rxBytes, quint64 txBytes); protected slots: diff --git a/client/protocols/ios_vpnprotocol.mm b/client/protocols/ios_vpnprotocol.mm index 14b7ee9a8..f314f5fbc 100644 --- a/client/protocols/ios_vpnprotocol.mm +++ b/client/protocols/ios_vpnprotocol.mm @@ -25,8 +25,10 @@ Proto currentProto = amnezia::Proto::Any; } IOSVpnProtocol::IOSVpnProtocol(Proto proto, const QJsonObject &configuration, QObject* parent) -: VpnProtocol(configuration, parent), -m_protocol(proto) {} +: VpnProtocol(configuration, parent), m_protocol(proto) +{ + connect(this, &IOSVpnProtocol::newTransmitedDataCount, this, &IOSVpnProtocol::setBytesChanged); +} IOSVpnProtocol* IOSVpnProtocol::instance() { return s_instance; @@ -207,8 +209,7 @@ void IOSVpnProtocol::checkStatus() qDebug() << "ServerIpv4Gateway:" << QString::fromNSString(serverIpv4Gateway) << "DeviceIpv4Address:" << QString::fromNSString(deviceIpv4Address) << "RxBytes:" << rxBytes << "TxBytes:" << txBytes; - emit bytesChanged(rxBytes, txBytes); - + emit newTransmitedDataCount(rxBytes, txBytes); }]; } diff --git a/client/protocols/vpnprotocol.cpp b/client/protocols/vpnprotocol.cpp index 2d304cdf7..a8f392e90 100644 --- a/client/protocols/vpnprotocol.cpp +++ b/client/protocols/vpnprotocol.cpp @@ -67,7 +67,10 @@ VpnProtocol::VpnConnectionState VpnProtocol::connectionState() const void VpnProtocol::setBytesChanged(quint64 receivedBytes, quint64 sentBytes) { - emit bytesChanged(receivedBytes - m_receivedBytes, sentBytes - m_sentBytes); + quint64 rxDiff = receivedBytes - m_receivedBytes; + quint64 txDiff = sentBytes - m_sentBytes; + + emit bytesChanged(rxDiff, txDiff); m_receivedBytes = receivedBytes; m_sentBytes = sentBytes; diff --git a/client/ui/pages_logic/QrDecoderLogic.cpp b/client/ui/pages_logic/QrDecoderLogic.cpp index 0c24ca1cc..764a6e4b3 100644 --- a/client/ui/pages_logic/QrDecoderLogic.cpp +++ b/client/ui/pages_logic/QrDecoderLogic.cpp @@ -3,10 +3,6 @@ #include "ui/uilogic.h" #include "ui/pages_logic/StartPageLogic.h" -#if defined(Q_OS_ANDROID) -#include "android_controller.h" -#endif - using namespace amnezia; using namespace PageEnumNS; diff --git a/client/ui/pages_logic/ServerSettingsLogic.cpp b/client/ui/pages_logic/ServerSettingsLogic.cpp index 22555a917..32a62e089 100644 --- a/client/ui/pages_logic/ServerSettingsLogic.cpp +++ b/client/ui/pages_logic/ServerSettingsLogic.cpp @@ -2,7 +2,6 @@ #include "vpnconnection.h" #include "../uilogic.h" -#include "ServerListLogic.h" #include "ShareConnectionLogic.h" #include "VpnLogic.h" @@ -11,9 +10,7 @@ #include #if defined(Q_OS_ANDROID) -#include -#include -#include +#include "../../platforms/android/androidutils.h" #endif ServerSettingsLogic::ServerSettingsLogic(UiLogic *logic, QObject *parent): @@ -24,9 +21,7 @@ ServerSettingsLogic::ServerSettingsLogic(UiLogic *logic, QObject *parent): m_pushButtonShareFullVisible{true}, m_pushButtonClearText{tr("Clear server from Amnezia software")}, m_pushButtonClearClientCacheText{tr("Clear client cached profile")} -{ - -} +{ } void ServerSettingsLogic::onUpdatePage() { @@ -140,7 +135,7 @@ void ServerSettingsLogic::onLineEditDescriptionEditingFinished() #if defined(Q_OS_ANDROID) /* Auth result handler for Android */ -void authResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) +void authResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) { qDebug() << "receiverRequestCode" << receiverRequestCode << "resultCode" << resultCode; @@ -155,16 +150,17 @@ void ServerSettingsLogic::onPushButtonShareFullClicked() { #if defined(Q_OS_ANDROID) /* We use builtin keyguard for ssh key export protection on Android */ - auto appContext = QtAndroid::androidActivity().callObjectMethod( + QJniObject activity = AndroidUtils::getActivity(); + auto appContext = activity.callObjectMethod( "getApplicationContext", "()Landroid/content/Context;"); if (appContext.isValid()) { QAndroidActivityResultReceiver *receiver = new authResultReceiver(uiLogic(), uiLogic()->selectedServerIndex); - auto intent = QAndroidJniObject::callStaticObjectMethod( + auto intent = QJniObject::callStaticObjectMethod( "org/amnezia/vpn/AuthHelper", "getAuthIntent", "(Landroid/content/Context;)Landroid/content/Intent;", appContext.object()); if (intent.isValid()) { if (intent.object() != nullptr) { - QtAndroid::startActivity(intent.object(), 1, receiver); + QtAndroidPrivate::startActivity(intent.object(), 1, receiver); } } else { uiLogic()->pageLogic()->updateSharingPage(uiLogic()->selectedServerIndex, DockerContainer::None); diff --git a/client/ui/pages_logic/ServerSettingsLogic.h b/client/ui/pages_logic/ServerSettingsLogic.h index e2d574224..d561ec48a 100644 --- a/client/ui/pages_logic/ServerSettingsLogic.h +++ b/client/ui/pages_logic/ServerSettingsLogic.h @@ -4,7 +4,8 @@ #include "PageLogicBase.h" #if defined(Q_OS_ANDROID) -#include +#include +#include #endif class UiLogic; @@ -52,7 +53,7 @@ public: ~authResultReceiver() {} public: - void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; + void handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) override; private: int m_serverIndex; diff --git a/client/ui/pages_logic/StartPageLogic.cpp b/client/ui/pages_logic/StartPageLogic.cpp index 76ba56fd6..3c15199de 100644 --- a/client/ui/pages_logic/StartPageLogic.cpp +++ b/client/ui/pages_logic/StartPageLogic.cpp @@ -11,8 +11,8 @@ #include #ifdef Q_OS_ANDROID -#include -#include "platforms/android/android_controller.h" +#include +#include "../../platforms/android/androidutils.h" #endif namespace { @@ -56,8 +56,9 @@ StartPageLogic::StartPageLogic(UiLogic *logic, QObject *parent): { #ifdef Q_OS_ANDROID // Set security screen for Android app - QtAndroid::runOnAndroidThread([]() { - QAndroidJniObject window = QtAndroid::androidActivity().callObjectMethod("getWindow", "()Landroid/view/Window;"); + AndroidUtils::runOnAndroidThreadSync([]() { + QJniObject activity = AndroidUtils::getActivity(); + QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); if (window.isValid()){ const int FLAG_SECURE = 8192; window.callMethod("addFlags", "(I)V", FLAG_SECURE); diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 07d81caf7..4ff67a070 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -19,7 +19,7 @@ #endif #ifdef Q_OS_ANDROID -#include "android_controller.h" +#include "../../platforms/android/android_controller.h" #include "protocols/android_vpnprotocol.h" #endif @@ -36,8 +36,6 @@ VpnConnection::VpnConnection(std::shared_ptr settings, m_settings(settings), m_configurator(configurator), m_serverController(serverController), - m_receivedBytes(0), - m_sentBytes(0), m_isIOSConnected(false) { } @@ -52,11 +50,7 @@ VpnConnection::~VpnConnection() void VpnConnection::onBytesChanged(quint64 receivedBytes, quint64 sentBytes) { - emit bytesChanged(receivedBytes - m_receivedBytes, sentBytes - m_sentBytes); - - m_receivedBytes = receivedBytes; - m_sentBytes = sentBytes; - + emit bytesChanged(receivedBytes, sentBytes); } void VpnConnection::onConnectionStateChanged(VpnProtocol::VpnConnectionState state) @@ -362,6 +356,7 @@ void VpnConnection::connectToVpn(int serverIndex, 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); m_vpnProtocol.reset(androidVpnProtocol); #elif defined Q_OS_IOS diff --git a/client/vpnconnection.h b/client/vpnconnection.h index c8ebdfad0..3a0d40646 100644 --- a/client/vpnconnection.h +++ b/client/vpnconnection.h @@ -93,8 +93,6 @@ private: QJsonObject m_vpnConfiguration; QJsonObject m_routeMode; QString m_remoteAddress; - quint64 m_receivedBytes; - quint64 m_sentBytes; bool m_isIOSConnected; //remove later move to isConnected, #ifdef AMNEZIA_DESKTOP diff --git a/deploy/build_android.sh b/deploy/build_android.sh index e87993941..4357034fc 100644 --- a/deploy/build_android.sh +++ b/deploy/build_android.sh @@ -20,46 +20,39 @@ APP_DOMAIN=org.amneziavpn.package OUT_APP_DIR=$BUILD_DIR/client BUNDLE_DIR=$OUT_APP_DIR/$APP_FILENAME -INSTALLER_DATA_DIR=$BUILD_DIR/installer/packages/$APP_DOMAIN/data -INSTALLER_BUNDLE_DIR=$BUILD_DIR/installer/$APP_FILENAME - -PRO_FILE_PATH=$PROJECT_DIR/$APP_NAME.pro -QMAKE_STASH_FILE=$PROJECT_DIR/.qmake_stash - # Seacrh Qt if [ -z "${QT_VERSION+x}" ]; then -QT_VERSION=5.15.2; -QT_BIN_DIR=$HOME/Qt/$QT_VERSION/android/bin +QT_VERSION=6.4.1; +QT_BIN_DIR=$HOME/Qt/$QT_VERSION/$ANDROID_CURRENT_ARCH/bin fi echo "Using Qt in $QT_BIN_DIR" echo "Using Android SDK in $ANDROID_SDK_ROOT" echo "Using Android NDK in $ANDROID_NDK_ROOT" -# Checking env -$QT_BIN_DIR/qmake -v -$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/make -v - # Build App echo "Building App..." cd $BUILD_DIR -$QT_BIN_DIR/qmake -r -spec android-clang CONFIG+=qtquickcompiler ANDROID_ABIS="armeabi-v7a arm64-v8a x86 x86_64" $PROJECT_DIR/AmneziaVPN.pro -echo "Executing make... may take long time" -$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/make -j2 -echo "Make install..." -$ANDROID_NDK_HOME/prebuilt/linux-x86_64/bin/make install INSTALL_ROOT=android -echo "Build OK" +echo "HOST Qt: $QT_HOST_PATH" -echo "............Deploy.................." +$QT_BIN_DIR/qt-cmake -S $PROJECT_DIR \ + -DQT_NO_GLOBAL_APK_TARGET_PART_OF_ALL="ON" \ + -DQT_HOST_PATH=$QT_HOST_PATH \ + -DCMAKE_BUILD_TYPE="Release" + +cmake --build . --config release + +echo "............APK generation.................." cd $OUT_APP_DIR -$QT_BIN_DIR/androiddeployqt \ - --output $OUT_APP_DIR/android \ +$QT_HOST_PATH/bin/androiddeployqt \ + --output $OUT_APP_DIR/android-build \ --gradle \ --release \ - --input android-AmneziaVPN-deployment-settings.json - + --input android-AmneziaVPN-deployment-settings.json \ + --android-platform android-31 + echo "............Copy apk.................." -cp $OUT_APP_DIR/android/build/outputs/apk/release/android-release-unsigned.apk \ +cp $OUT_APP_DIR/android-build/build/outputs/apk/release/android-build-release-unsigned.apk \ $PROJECT_DIR/AmneziaVPN-release-unsigned.apk