diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index e3adf67a8..5af762d7c 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -91,8 +91,8 @@ void AmneziaApplication::init() initControllers(); #ifdef Q_OS_ANDROID - connect(AndroidController::instance(), &AndroidController::initialized, this, - [this](bool status, bool connected, const QDateTime &connectionDate) { + connect(AndroidController::instance(), &AndroidController::serviceIsAlive, this, + [this](bool connected) { if (connected) { m_connectionController->onConnectionStateChanged(Vpn::ConnectionState::Connected); if (m_vpnConnection) diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 80eb2b6c7..c65f4bee4 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -1,286 +1,33 @@ -/* 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 -import android.Manifest -import android.content.ComponentName -import android.content.ContentResolver -import android.content.Context import android.content.Intent -import android.content.ServiceConnection -import android.content.pm.PackageManager import android.net.Uri -import android.os.* -import android.provider.MediaStore -import android.util.Log -import android.view.KeyEvent -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -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 java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException import org.qtproject.qt.android.bindings.QtActivity -import java.io.* -class AmneziaActivity : org.qtproject.qt.android.bindings.QtActivity() { +private const val TAG = "AmneziaActivity" - private var configString: String? = null - private var vpnServiceBinder: IBinder? = null - private var isBound = false - set(value) { - field = value +private const val CAMERA_ACTION_CODE = 101 +private const val CREATE_FILE_ACTION_CODE = 102 - if (value && configString != null) { - sendImportConfigCommand() - } - } - - private val TAG = "AmneziaActivity" - - private val CAMERA_ACTION_CODE = 101 - private val CREATE_FILE_ACTION_CODE = 102 +class AmneziaActivity : QtActivity() { private var tmpFileContentToSave: String = "" - private val delayedCommands: ArrayList> = ArrayList() - - companion object { - private lateinit var instance: AmneziaActivity - - @JvmStatic fun getInstance(): AmneziaActivity { - return instance - } - - @JvmStatic fun connectService() { - getInstance().initServiceConnection() - } - - @JvmStatic fun startQrCodeReader() { - getInstance().startQrCodeActivity() - } - - @JvmStatic fun sendToService(actionCode: Int, body: String) { - getInstance().dispatchParcel(actionCode, body) - } - - @JvmStatic fun saveFileAs(fileContent: String, suggestedName: String) { - getInstance().saveFile(fileContent, suggestedName) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - instance = this - - val newIntent = intent - val newIntentAction: String? = newIntent.action - - if (newIntent != null && newIntentAction != null && newIntentAction == "org.amnezia.vpn.IMPORT_CONFIG") { - configString = newIntent.getStringExtra("CONFIG") - } - } - private fun startQrCodeActivity() { val intent = Intent(this, CameraActivity::class.java) 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) - } - - external fun handleBackButton(): Boolean - - external fun onServiceMessage(actionCode: Int, body: String?) - external fun qtOnServiceConnected() - external fun qtOnServiceDisconnected() - external fun onActivityMessage(actionCode: Int, body: String?) - - private fun dispatchParcel(actionCode: Int, body: String) { - if (!isBound) { - Log.d(TAG, "dispatchParcel: not bound") - delayedCommands.add(Pair(actionCode, body)) - return - } - - 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()) - - 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) { - super.onNewIntent(intent) - - setIntent(newIntent) - - val newIntentAction = newIntent.action - - if (newIntent != null && newIntentAction != null && newIntentAction == INTENT_ACTION_IMPORT_CONFIG) { - configString = newIntent.getStringExtra("CONFIG") - } - } - - override fun onResume() { - super.onResume() - - if (configString != null && isBound) { - sendImportConfigCommand() - } - } - - 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 fun createConnection() = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, binder: IBinder) { - vpnServiceBinder = binder - - // 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 - } - - override fun onServiceDisconnected(className: ComponentName) { - vpnServiceBinder = null - isBound = false - qtOnServiceDisconnected(); - } - } - - private var connection: ServiceConnection = createConnection() - - 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, AmneziaVpnService::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_REQUIRED = 6 - private val EVENT_DISCONNECTED = 2 - - private val UI_EVENT_QR_CODE_RECEIVED = 0 - - fun onPermissionRequest(code: Int, data: Parcel?) { - if (code != EVENT_PERMISSION_REQUIRED) { - 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) - - if (requestCode == CAMERA_ACTION_CODE && resultCode == RESULT_OK) { - val extra = data?.getStringExtra("result") ?: "" - onActivityMessage(UI_EVENT_QR_CODE_RECEIVED, extra) - } - + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == CREATE_FILE_ACTION_CODE && resultCode == RESULT_OK) { data?.data?.also { uri -> alterDocument(uri) } } + super.onActivityResult(requestCode, resultCode, data) } private fun alterDocument(uri: Uri) { @@ -298,4 +45,58 @@ class AmneziaActivity : org.qtproject.qt.android.bindings.QtActivity() { tmpFileContentToSave = "" } + + /** + * Methods called by Qt + */ + @Suppress("unused") + fun qtAndroidControllerInitialized() { + Log.v(TAG, "Qt Android controller initialized") + Log.w(TAG, "Not yet implemented") + } + + @Suppress("unused") + fun start(vpnConfig: String) { + Log.v(TAG, "Start VPN") + Log.w(TAG, "Not yet implemented") + } + + @Suppress("unused") + fun stop() { + Log.v(TAG, "Stop VPN") + Log.w(TAG, "Not yet implemented") + } + + @Suppress("unused") + fun saveFile(fileName: String, data: String) { + Log.v(TAG, "Save file $fileName") + // todo: refactor + tmpFileContentToSave = data + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/*" + putExtra(Intent.EXTRA_TITLE, fileName) + } + + startActivityForResult(intent, CREATE_FILE_ACTION_CODE) + } + + @Suppress("unused") + fun setNotificationText(title: String, message: String, timerSec: Int) { + Log.v(TAG, "Set notification text") + Log.w(TAG, "Not yet implemented") + } + + @Suppress("unused") + fun cleanupLogs() { + Log.v(TAG, "Cleanup logs") + Log.w(TAG, "Not yet implemented") + } + + @Suppress("unused") + fun startQrCodeReader() { + Log.v(TAG, "Start camera") + startQrCodeActivity() + } } diff --git a/client/android/src/org/amnezia/vpn/VPNClientBinder.kt b/client/android/src/org/amnezia/vpn/VPNClientBinder.kt deleted file mode 100644 index 41a0000cf..000000000 --- a/client/android/src/org/amnezia/vpn/VPNClientBinder.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* 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 - -import android.os.Binder -import android.os.Parcel - -const val permissionRequired = 6 - -class VPNClientBinder() : Binder() { - - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if (code == permissionRequired) { - AmneziaActivity.getInstance().onPermissionRequest(code, data) - return true - } - - val buffer = data.createByteArray() - val stringData = buffer?.let { String(it) } - AmneziaActivity.getInstance().onServiceMessage(code, stringData) - - return true - } -} diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index 964242508..ce509281c 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -1,4 +1,18 @@ package org.amnezia.vpn.qt +/** + * JNI functions of the AndroidController class from android_controller.cpp, + * called by events in the Android part of the client + */ object QtAndroidController { + external fun onStatus(isVpnConnected: Boolean) + external fun onServiceDisconnected() + external fun onServiceError() + + external fun onVpnPermissionRejected() + external fun onVpnConnected() + external fun onVpnDisconnected() + external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) + + external fun onConfigImported() } \ No newline at end of file diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake index 66ff1e15a..2d08b4b6d 100644 --- a/client/cmake/android.cmake +++ b/client/cmake/android.cmake @@ -28,7 +28,6 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidvpnactivity.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h ) @@ -37,7 +36,6 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidvpnactivity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp ) diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 17264d122..5bf0be581 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -1,154 +1,82 @@ -/* 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 +#include +#include #include -#include -#include -#include -#include #include "android_controller.h" -#include "private/qandroidextras_p.h" - -#include "androidutils.h" -#include "androidvpnactivity.h" namespace { AndroidController *s_instance = nullptr; - constexpr auto PERMISSIONHELPER_CLASS = "org/amnezia/vpn/VPNPermissionHelper"; + constexpr auto QT_ANDROID_CONTROLLER_CLASS = "org/amnezia/vpn/qt/QtAndroidController"; } // namespace 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(); - - if (isConnected) { - emit scheduleStatusCheckSignal(); - } - - 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"; - - if (!isConnected) { - emit scheduleStatusCheckSignal(); - } - - isConnected = true; - - emit connectionStateChanged(Vpn::ConnectionState::Connected); - }, - Qt::QueuedConnection); - - connect( - activity, &AndroidVPNActivity::eventDisconnected, this, - [this]() { - qDebug() << "Transact: disconnected"; - - isConnected = false; - - emit connectionStateChanged(Vpn::ConnectionState::Disconnected); - }, - Qt::QueuedConnection); - - connect( - activity, &AndroidVPNActivity::eventStatisticUpdate, this, - [this](const QString &parcelBody) { - 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); + connect(this, &AndroidController::status, this, + [this](bool isVpnConnected) { + qDebug() << "Android event: status; connected:" << isVpnConnected; + if (isWaitingInitStatus) { + qDebug() << "Android VPN service is alive, initialization by service status"; + isWaitingInitStatus = false; + emit serviceIsAlive(isVpnConnected); } }, Qt::QueuedConnection); connect( - activity, &AndroidVPNActivity::eventActivationError, this, - [this](const QString &parcelBody) { - Q_UNUSED(parcelBody) - qDebug() << "Transact: error"; - emit connectionStateChanged(Vpn::ConnectionState::Error); - }, - Qt::QueuedConnection); + this, &AndroidController::serviceDisconnected, this, + [this]() { + qDebug() << "Android event: service disconnected"; + emit connectionStateChanged(Vpn::ConnectionState::Unknown); + }, + 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; - importConfigFromOutside(buffer); - }, - Qt::QueuedConnection); + this, &AndroidController::serviceError, this, + [this]() { + qDebug() << "Android event: service error"; + // todo: add error message + emit connectionStateChanged(Vpn::ConnectionState::Error); + }, + Qt::QueuedConnection); connect( - activity, &AndroidVPNActivity::serviceDisconnected, this, - [this]() { - qDebug() << "Transact: service disconnected"; - m_serviceConnected = false; - }, - Qt::QueuedConnection); + this, &AndroidController::vpnPermissionRejected, this, + [this]() { + qWarning() << "Android event: VPN permission rejected"; + emit connectionStateChanged(Vpn::ConnectionState::Disconnected); + }, + Qt::QueuedConnection); + + connect( + this, &AndroidController::vpnConnected, this, + [this]() { + qDebug() << "Android event: VPN connected"; + emit connectionStateChanged(Vpn::ConnectionState::Connected); + }, + Qt::QueuedConnection); + + connect( + this, &AndroidController::vpnDisconnected, this, + [this]() { + qDebug() << "Android event: VPN disconnected"; + emit connectionStateChanged(Vpn::ConnectionState::Disconnected); + }, + Qt::QueuedConnection); + + connect( + this, &AndroidController::configImported, this, + []() { + // todo: not yet implemented + qDebug() << "Transact: config import"; + /*auto doc = QJsonDocument::fromJson(parcelBody.toUtf8()); + + QString buffer = doc.object()["config"].toString(); + qDebug() << "Transact: config string" << buffer; + importConfigFromOutside(buffer);*/ + }, + Qt::QueuedConnection); } AndroidController *AndroidController::instance() @@ -162,166 +90,160 @@ AndroidController *AndroidController::instance() bool AndroidController::initialize() { - qDebug() << "Initializing"; + qDebug() << "Initialize AndroidController"; + + const JNINativeMethod methods[] = { + {"onStatus", "(Z)V", reinterpret_cast(onStatus)}, + {"onServiceDisconnected", "()V", reinterpret_cast(onServiceDisconnected)}, + {"onServiceError", "()V", reinterpret_cast(onServiceError)}, + {"onVpnPermissionRejected", "()V", reinterpret_cast(onVpnPermissionRejected)}, + {"onVpnConnected", "()V", reinterpret_cast(onVpnConnected)}, + {"onVpnDisconnected", "()V", reinterpret_cast(onVpnDisconnected)}, + {"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)}, + {"onConfigImported", "()V", reinterpret_cast(onConfigImported)}, + }; - // Hook in the native implementation for startActivityForResult into the JNI - JNINativeMethod methods[] { { "startActivityForResult", "(Landroid/content/Intent;)V", - reinterpret_cast(startActivityForResult) } }; - QJniObject javaClass(PERMISSIONHELPER_CLASS); QJniEnvironment env; - jclass objectClass = env->GetObjectClass(javaClass.object()); - env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0])); - env->DeleteLocalRef(objectClass); - - AndroidVPNActivity::connectService(); - + bool registered = env.registerNativeMethods(QT_ANDROID_CONTROLLER_CLASS, methods, + sizeof(methods) / sizeof(JNINativeMethod)); + if (!registered) { + qCritical() << "Failed native method registration"; + return false; + } + qtAndroidControllerInitialized(); return true; } -ErrorCode AndroidController::start() +// static +template +auto AndroidController::callActivityMethod(const char *methodName, const char *signature, + const std::function &defValue, Args &&...args) { - qDebug() << "Prompting for VPN permission"; - QJniObject activity = AndroidUtils::getActivity(); - auto appContext = activity.callObjectMethod("getApplicationContext", "()Landroid/content/Context;"); - QJniObject::callStaticMethod(PERMISSIONHELPER_CLASS, "startService", "(Landroid/content/Context;)V", - appContext.object()); + qDebug() << "Call activity method:" << methodName; + QJniObject activity = QNativeInterface::QAndroidApplication::context(); + if (activity.isValid()) { + return activity.callMethod(methodName, signature, std::forward(args)...); + } else { + qCritical() << "Activity is not valid"; + return defValue(); + } +} - QJsonDocument doc(m_vpnConfig); - AndroidVPNActivity::sendToService(ServiceAction::ACTION_ACTIVATE, doc.toJson()); +// static +template +void AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args) +{ + callActivityMethod(methodName, signature, [] {}, std::forward(args)...); +} + +ErrorCode AndroidController::start(const QJsonObject &vpnConfig) +{ + isWaitingInitStatus = false; + auto config = QJsonDocument(vpnConfig).toJson(); + callActivityMethod("start", "(Ljava/lang/String;)V", + QJniObject::fromString(config).object()); return NoError; } void AndroidController::stop() { - qDebug() << "AndroidController::stop"; - - AndroidVPNActivity::sendToService(ServiceAction::ACTION_DEACTIVATE, QString()); + callActivityMethod("stop", "()V"); } -// Activates the tunnel that is currently set -// in the VPN Service -void AndroidController::resumeStart() +void AndroidController::saveFile(const QString &fileName, const QString &data) { - AndroidVPNActivity::sendToService(ServiceAction::ACTION_RESUME_ACTIVATE, QString()); + callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V", + QJniObject::fromString(fileName).object(), + QJniObject::fromString(data).object()); } -/* - * Sets the current notification text that is shown - */ 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); - - AndroidVPNActivity::sendToService(ServiceAction::ACTION_SET_NOTIFICATION_TEXT, doc.toJson()); -} - -void AndroidController::shareConfig(const QString &configContent, const QString &suggestedName) -{ - AndroidVPNActivity::saveFileAs(configContent, suggestedName); -} - -/* - * Sets fallback Notification text that should be shown in case the VPN - * switches into the Connected state without the app open - * 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); - - AndroidVPNActivity::sendToService(ServiceAction::ACTION_SET_NOTIFICATION_FALLBACK, doc.toJson()); -} - -void AndroidController::checkStatus() -{ - AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_STATISTIC, QString()); -} - -void AndroidController::getBackendLogs(std::function &&a_callback) -{ - qDebug() << "get logs"; - - m_logCallback = std::move(a_callback); - - AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_GET_LOG, QString()); -} - -void AndroidController::cleanupBackendLogs() -{ - qDebug() << "cleanup logs"; - - AndroidVPNActivity::sendToService(ServiceAction::ACTION_REQUEST_CLEANUP_LOG, QString()); -} - -const QJsonObject &AndroidController::vpnConfig() const -{ - return m_vpnConfig; -} - -void AndroidController::setVpnConfig(const QJsonObject &newVpnConfig) -{ - m_vpnConfig = newVpnConfig; + callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V", + QJniObject::fromString(title).object(), + QJniObject::fromString(message).object(), + (jint) timerSec); } void AndroidController::startQrReaderActivity() { - AndroidVPNActivity::instance()->startQrCodeReader(); + callActivityMethod("startQrCodeReader", "()V"); } -void AndroidController::scheduleStatusCheckSlot() +void AndroidController::qtAndroidControllerInitialized() { - QTimer::singleShot(1000, [this]() { - if (isConnected) { - checkStatus(); - emit scheduleStatusCheckSignal(); - } - }); + callActivityMethod("qtAndroidControllerInitialized", "()V"); } -const int ACTIVITY_RESULT_OK = 0xffffffff; -/** - * @brief Starts the Given intent in Context of the QTActivity - * @param env - * @param intent - */ -void AndroidController::startActivityForResult(JNIEnv *env, jobject, jobject intent) +// JNI functions called by Android +// static +void AndroidController::onStatus(JNIEnv *env, jobject thiz, jboolean isVpnConnected) { - qDebug() << "start vpnPermissionHelper"; Q_UNUSED(env); + Q_UNUSED(thiz); - QtAndroidPrivate::startActivity(intent, 1337, [](int receiverRequestCode, int resultCode, const QJniObject &data) { - // Currently this function just used in - // VPNService.kt::checkPermissions. 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; - } - - 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(Vpn::ConnectionState::Disconnected); - }); - return; + emit AndroidController::instance()->status(isVpnConnected); +} + +// static +void AndroidController::onServiceDisconnected(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->serviceDisconnected(); +} + +// static +void AndroidController::onServiceError(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->serviceError(); +} + +// static +void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->vpnPermissionRejected(); +} + +// static +void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->vpnConnected(); +} + +// static +void AndroidController::onVpnDisconnected(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->vpnDisconnected(); +} + +// static +void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->statisticsUpdated((quint64) rxBytes, (quint64) txBytes); +} + +void AndroidController::onConfigImported(JNIEnv *env, jobject thiz) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + emit AndroidController::instance()->configImported(); } diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index baa0bc80b..92d1916dd 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -1,11 +1,6 @@ -/* 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 "protocols/vpnprotocol.h" @@ -20,57 +15,47 @@ public: explicit AndroidController(); static AndroidController *instance(); - virtual ~AndroidController() override = default; - bool initialize(); - ErrorCode start(); + ErrorCode start(const QJsonObject &vpnConfig); void stop(); - void resumeStart(); - - void checkStatus(); void setNotificationText(const QString &title, const QString &message, int timerSec); - void shareConfig(const QString &data, const QString &suggestedName); - void setFallbackConnectedNotification(); - void getBackendLogs(std::function &&callback); - void cleanupBackendLogs(); - - const QJsonObject &vpnConfig() const; - void setVpnConfig(const QJsonObject &newVpnConfig); - + void saveFile(const QString& fileName, const QString &data); void startQrReaderActivity(); signals: void connectionStateChanged(Vpn::ConnectionState state); - - // This signal is emitted when the controller is initialized. Note that the - // VPN tunnel can be already active. In this case, "connected" should be set - // to true and the "connectionDate" should be set to the activation date if - // known. - // If "status" is set to false, the backend service is considered unavailable. - void initialized(bool status, bool connected, const QDateTime &connectionDate); - - void statusUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4); - void scheduleStatusCheckSignal(); - + void status(bool isVpnConnected); + void serviceDisconnected(); + void serviceError(); + void vpnPermissionRejected(); + void vpnConnected(); + void vpnDisconnected(); + void statisticsUpdated(quint64 rxBytes, quint64 txBytes); + void configImported(); void importConfigFromOutside(QString &data); - -protected slots: - void scheduleStatusCheckSlot(); + void serviceIsAlive(bool connected); private: - bool m_init = false; + bool isWaitingInitStatus = true; - QJsonObject m_vpnConfig; + void qtAndroidControllerInitialized(); - bool m_serviceConnected = false; - std::function m_logCallback; + // JNI functions called by Android + static void onStatus(JNIEnv *env, jobject thiz, jboolean isVpnConnected); + static void onServiceDisconnected(JNIEnv *env, jobject thiz); + static void onServiceError(JNIEnv *env, jobject thiz); + static void onVpnPermissionRejected(JNIEnv *env, jobject thiz); + static void onVpnConnected(JNIEnv *env, jobject thiz); + static void onVpnDisconnected(JNIEnv *env, jobject thiz); + static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); + static void onConfigImported(JNIEnv *env, jobject thiz); - static void startActivityForResult(JNIEnv *env, jobject /*thiz*/, jobject intent); - - bool isConnected = false; - - void scheduleStatusCheck(); + template + static auto callActivityMethod(const char *methodName, const char *signature, + const std::function &defValue, Args &&...args); + template + static void callActivityMethod(const char *methodName, const char *signature, Args &&...args); }; #endif // ANDROID_CONTROLLER_H diff --git a/client/platforms/android/androidvpnactivity.cpp b/client/platforms/android/androidvpnactivity.cpp deleted file mode 100644 index 94d7eddb0..000000000 --- a/client/platforms/android/androidvpnactivity.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/* 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 "androidutils.h" -#include "jni.h" - -namespace -{ - AndroidVPNActivity *s_instance = nullptr; - constexpr auto CLASSNAME = "org.amnezia.vpn.AmneziaActivity"; -} - -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) }, - { "onActivityMessage", "(ILjava/lang/String;)V", reinterpret_cast(onAndroidVpnActivityMessage) } - }; - - 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"); -} - -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()); -} - -// 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; - - 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::handleActivityMessage(int code, const QString &data) -{ - auto mode = (UIEvents)code; - - switch (mode) { - case UIEvents::QR_CODED_DECODED: emit eventQrCodeReceived(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(); -} - -void AndroidVPNActivity::onAndroidVpnActivityMessage(JNIEnv *env, jobject thiz, jint messageType, jstring message) -{ - Q_UNUSED(thiz); - const char *buffer = env->GetStringUTFChars(message, nullptr); - if (!buffer) { - return; - } - - QString parcelBody(buffer); - env->ReleaseStringUTFChars(message, buffer); - - AndroidUtils::dispatchToMainThread([messageType, parcelBody] { - AndroidVPNActivity::instance()->handleActivityMessage(messageType, parcelBody); - }); -} diff --git a/client/platforms/android/androidvpnactivity.h b/client/platforms/android/androidvpnactivity.h deleted file mode 100644 index c485f4e56..000000000 --- a/client/platforms/android/androidvpnactivity.h +++ /dev/null @@ -1,104 +0,0 @@ -/* 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 receive 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 -// Kotlin codes in the VPNServiceBinder.kt -enum ServiceEvents { - // The Service has Accepted 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 transferred 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; - -enum UIEvents { - QR_CODED_DECODED = 0, -}; -typedef enum UIEvents UIEvents; - -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(); - static void startQrCodeReader(); - static void saveFileAs(QString fileContent, QString suggestedFilename); - -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); - void eventQrCodeReceived(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); - static void onAndroidVpnActivityMessage(JNIEnv* env, jobject thiz, jint messageType, jstring message); - void handleServiceMessage(int code, const QString& data); - void handleActivityMessage(int code, const QString& data); -}; - -#endif // ANDROIDVPNACTIVITY_H diff --git a/client/protocols/android_vpnprotocol.cpp b/client/protocols/android_vpnprotocol.cpp index 521b08f2e..2c09dd8e9 100644 --- a/client/protocols/android_vpnprotocol.cpp +++ b/client/protocols/android_vpnprotocol.cpp @@ -1,26 +1,16 @@ -#include -#include -#include -#include -#include -#include -#include -#include - #include "android_vpnprotocol.h" #include "platforms/android/android_controller.h" AndroidVpnProtocol::AndroidVpnProtocol(Proto protocol, const QJsonObject &configuration, QObject* parent) - : VpnProtocol(configuration, parent), - m_protocol(protocol) + : VpnProtocol(configuration, parent) { } ErrorCode AndroidVpnProtocol::start() { - AndroidController::instance()->setVpnConfig(m_rawConfig); - return AndroidController::instance()->start(); + qDebug() << "AndroidVpnProtocol::start()"; + return AndroidController::instance()->start(m_rawConfig); } void AndroidVpnProtocol::stop() @@ -29,11 +19,8 @@ void AndroidVpnProtocol::stop() AndroidController::instance()->stop(); } -void AndroidVpnProtocol::connectionDataUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4) +void AndroidVpnProtocol::connectionDataUpdated(quint64 rxBytes, quint64 txBytes) { - 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 ea87679a6..9d4e26cd0 100644 --- a/client/protocols/android_vpnprotocol.h +++ b/client/protocols/android_vpnprotocol.h @@ -2,7 +2,6 @@ #define ANDROID_VPNPROTOCOL_H #include "vpnprotocol.h" -#include "protocols/protocols_defs.h" using namespace amnezia; @@ -12,24 +11,14 @@ class AndroidVpnProtocol : public VpnProtocol public: explicit AndroidVpnProtocol(Proto protocol, const QJsonObject& configuration, QObject* parent = nullptr); - virtual ~AndroidVpnProtocol() override = default; + ~AndroidVpnProtocol() override = default; ErrorCode start() override; void stop() override; -signals: - - public slots: - void connectionDataUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4); + void connectionDataUpdated(quint64 rxBytes, quint64 txBytes); -protected slots: - -protected: - - -private: - Proto m_protocol; }; #endif // ANDROID_VPNPROTOCOL_H diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 7de071cca..96fc27925 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -27,7 +27,7 @@ SystemController::SystemController(const std::shared_ptr &settings, QO void SystemController::saveFile(QString fileName, const QString &data) { #if defined Q_OS_ANDROID - AndroidController::instance()->shareConfig(data, fileName); + AndroidController::instance()->saveFile(fileName, data); return; #endif diff --git a/client/vpnconnection.cpp b/client/vpnconnection.cpp index 2505a9ea3..56f5c2b62 100644 --- a/client/vpnconnection.cpp +++ b/client/vpnconnection.cpp @@ -409,7 +409,7 @@ void VpnConnection::createAndroidConnections(DockerContainer container) connect(AndroidController::instance(), &AndroidController::connectionStateChanged, androidVpnProtocol, &AndroidVpnProtocol::setConnectionState); - connect(AndroidController::instance(), &AndroidController::statusUpdated, androidVpnProtocol, + connect(AndroidController::instance(), &AndroidController::statisticsUpdated, androidVpnProtocol, &AndroidVpnProtocol::connectionDataUpdated); }