Merge branch 'dev' of github.com:amnezia-vpn/desktop-client into feature/qt6-libssh-support

This commit is contained in:
vladimir.kuznetsov 2023-02-27 19:53:53 +03:00
commit a287192649
24 changed files with 461 additions and 222 deletions

View file

@ -577,6 +577,16 @@ if(NOT IOS AND NOT ANDROID)
COMMAND_EXPAND_LISTS COMMAND_EXPAND_LISTS
) )
endif() endif()
if(WIN32)
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy,true>
$<TARGET_FILE_DIR:${PROJECT}>/../service/wireguard-service/wireguard-service.exe
$<TARGET_FILE_DIR:${PROJECT}>/wireguard/wireguard-service.exe
COMMAND_EXPAND_LISTS
)
endif()
if(IOS) if(IOS)
#include(cmake/ios-arch-fixup.cmake) #include(cmake/ios-arch-fixup.cmake)
endif() endif()

View file

@ -34,6 +34,7 @@
android:extractNativeLibs="true" android:extractNativeLibs="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false" android:allowNativeHeapPointerTagging="false"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:icon="@drawable/icon"> android:icon="@drawable/icon">
<activity <activity
@ -113,6 +114,10 @@
android:value="-- %%INSERT_APP_ARGUMENTS%% --" /> android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
</activity> </activity>
<activity
android:name=".qt.CameraActivity"
android:exported="false" />
<service <service
android:name=".VPNService" android:name=".VPNService"

View file

@ -2,7 +2,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext{ ext{
kotlin_version = "1.4.30-M1" kotlin_version = "1.7.22"
// for libwg // for libwg
appcompatVersion = '1.1.0' appcompatVersion = '1.1.0'
annotationsVersion = '1.0.1' annotationsVersion = '1.0.1'
@ -28,12 +28,6 @@ buildscript {
} }
} }
repositories {
google()
jcenter()
mavenCentral()
}
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
@ -42,14 +36,34 @@ apply plugin: 'kotlin-kapt'
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation "androidx.security:security-crypto:1.1.0-alpha03" implementation "androidx.security:security-crypto:1.1.0-alpha03"
implementation "androidx.security:security-identity-credential:1.0.0-alpha02" implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
implementation project(path: ':shadowsocks') implementation project(path: ':shadowsocks')
// CameraX core library using the camera2 implementation
def camerax_version = "1.2.1"
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
implementation("androidx.camera:camera-view:${camerax_version}")
implementation("androidx.camera:camera-extensions:${camerax_version}")
def camerax_ml_version = "1.2.0-beta02"
def ml_kit_version = "17.0.3"
implementation("androidx.camera:camera-mlkit-vision:${camerax_ml_version}")
implementation("com.google.mlkit:barcode-scanning:${ml_kit_version}")
} }
androidExtensions { androidExtensions {

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1 +1,19 @@
pluginManagement {
repositories {
google()
mavenCentral()
jcenter()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
jcenter()
}
}
include ':shadowsocks' include ':shadowsocks'

View file

@ -1,21 +1,9 @@
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
//apply plugin: 'com.novoda.bintray-release' //apply plugin: 'com.novoda.bintray-release'
android { android {
compileSdkVersion 30 compileSdkVersion 30
defaultConfig { defaultConfig {

View file

@ -0,0 +1,158 @@
package org.amnezia.vpn.qt
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import org.amnezia.vpn.R
class CameraActivity : AppCompatActivity() {
private val CAMERA_REQUEST = 100
private lateinit var cameraExecutor: ExecutorService
private lateinit var analyzerExecutor: ExecutorService
private lateinit var viewFinder: PreviewView
companion object {
private lateinit var instance: CameraActivity
@JvmStatic fun getInstance(): CameraActivity {
return instance
}
@JvmStatic fun stopQrCodeReader() {
CameraActivity.getInstance().finish()
}
}
external fun passDataToDecoder(data: String)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
viewFinder = findViewById(R.id.viewFinder)
cameraExecutor = Executors.newSingleThreadExecutor()
analyzerExecutor = Executors.newSingleThreadExecutor()
instance = this
checkPermissions()
configureVideoPreview()
}
private fun checkPermissions() {
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_REQUEST)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == CAMERA_REQUEST) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "CameraX permission granted", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "CameraX permission denied", Toast.LENGTH_SHORT).show();
}
}
}
@SuppressLint("UnsafeOptInUsageError")
private fun configureVideoPreview() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
val imageCapture = ImageCapture.Builder().build()
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val imageAnalyzer = BarCodeAnalyzer()
val analysisUseCase = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysisUseCase.setAnalyzer(analyzerExecutor, imageAnalyzer)
try {
preview.setSurfaceProvider(viewFinder.surfaceProvider)
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, analysisUseCase)
} catch(exc: Exception) {
Log.e("WUTT", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
override fun onDestroy() {
cameraExecutor.shutdown()
analyzerExecutor.shutdown()
super.onDestroy()
}
val barcodesSet = mutableSetOf<String>()
private inner class BarCodeAnalyzer(): ImageAnalysis.Analyzer {
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
private val scanner = BarcodeScanning.getClient(options)
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(image)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
val barcode = barcodes[0]
if (barcode != null) {
val str = barcode?.displayValue ?: ""
if (str.isNotEmpty()) {
val isAdded = barcodesSet.add(str)
if (isAdded) {
passDataToDecoder(str)
}
}
}
}
imageProxy.close()
}
.addOnFailureListener {
imageProxy.close()
}
}
}
}
}

View file

@ -36,6 +36,8 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private val TAG = "VPNActivity" private val TAG = "VPNActivity"
private val STORAGE_PERMISSION_CODE = 42 private val STORAGE_PERMISSION_CODE = 42
private val CAMERA_ACTION_CODE = 101
companion object { companion object {
private lateinit var instance: VPNActivity private lateinit var instance: VPNActivity
@ -47,6 +49,10 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
VPNActivity.getInstance().initServiceConnection() VPNActivity.getInstance().initServiceConnection()
} }
@JvmStatic fun startQrCodeReader() {
VPNActivity.getInstance().startQrCodeActivity()
}
@JvmStatic fun sendToService(actionCode: Int, body: String) { @JvmStatic fun sendToService(actionCode: Int, body: String) {
VPNActivity.getInstance().dispatchParcel(actionCode, body) VPNActivity.getInstance().dispatchParcel(actionCode, body)
} }
@ -62,7 +68,12 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = this; instance = this
}
private fun startQrCodeActivity() {
val intent = Intent(this, CameraActivity::class.java)
startActivityForResult(intent, CAMERA_ACTION_CODE)
} }
override fun getSystemService(name: String): Any? { override fun getSystemService(name: String): Any? {
@ -82,6 +93,7 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
external fun onServiceMessage(actionCode: Int, body: String?) external fun onServiceMessage(actionCode: Int, body: String?)
external fun qtOnServiceConnected() external fun qtOnServiceConnected()
external fun qtOnServiceDisconnected() external fun qtOnServiceDisconnected()
external fun onActivityMessage(actionCode: Int, body: String?)
private fun dispatchParcel(actionCode: Int, body: String) { private fun dispatchParcel(actionCode: Int, body: String) {
if (!isBound) { if (!isBound) {
@ -286,6 +298,7 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private val EVENT_PERMISSION_REQURED = 6 private val EVENT_PERMISSION_REQURED = 6
private val EVENT_DISCONNECTED = 2 private val EVENT_DISCONNECTED = 2
private val UI_EVENT_QR_CODE_RECEIVED = 0
fun onPermissionRequest(code: Int, data: Parcel?) { fun onPermissionRequest(code: Int, data: Parcel?) {
if (code != EVENT_PERMISSION_REQURED) { if (code != EVENT_PERMISSION_REQURED) {
@ -310,7 +323,13 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
return return
} }
super.onActivityResult(requestCode, resultCode, data) 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 onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {

View file

@ -27,6 +27,16 @@ QString ContainerProps::containerToString(amnezia::DockerContainer c){
return "amnezia-" + containerKey.toLower(); return "amnezia-" + containerKey.toLower();
} }
QString ContainerProps::containerTypeToString(amnezia::DockerContainer c){
if (c == DockerContainer::None) return "none";
if (c == DockerContainer::Ipsec) return "ikev2";
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
return containerKey.toLower();
}
QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerContainer container) QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerContainer container)
{ {
switch (container) { switch (container) {
@ -171,3 +181,10 @@ return false;
#endif #endif
} }
QStringList ContainerProps::fixedPortsForContainer(DockerContainer c)
{
switch (c) {
case DockerContainer::Ipsec : return QStringList{"500", "4500"};
default: return {};
}
}

View file

@ -37,24 +37,26 @@ class ContainerProps : public QObject
Q_OBJECT Q_OBJECT
public: public:
Q_INVOKABLE static DockerContainer containerFromString(const QString &container); Q_INVOKABLE static amnezia::DockerContainer containerFromString(const QString &container);
Q_INVOKABLE static QString containerToString(DockerContainer container); Q_INVOKABLE static QString containerToString(amnezia::DockerContainer container);
Q_INVOKABLE static QString containerTypeToString(amnezia::DockerContainer c);
Q_INVOKABLE static QList<DockerContainer> allContainers(); Q_INVOKABLE static QList<amnezia::DockerContainer> allContainers();
Q_INVOKABLE static QMap<DockerContainer, QString> containerHumanNames(); Q_INVOKABLE static QMap<amnezia::DockerContainer, QString> containerHumanNames();
Q_INVOKABLE static QMap<DockerContainer, QString> containerDescriptions(); Q_INVOKABLE static QMap<amnezia::DockerContainer, QString> containerDescriptions();
// these protocols will be displayed in container settings // these protocols will be displayed in container settings
Q_INVOKABLE static QVector<Proto> protocolsForContainer(DockerContainer container); Q_INVOKABLE static QVector<amnezia::Proto> protocolsForContainer(amnezia::DockerContainer container);
Q_INVOKABLE static ServiceType containerService(DockerContainer c); Q_INVOKABLE static amnezia::ServiceType containerService(amnezia::DockerContainer c);
// binding between Docker container and main protocol of given container // binding between Docker container and main protocol of given container
// it may be changed fot future containers :) // it may be changed fot future containers :)
Q_INVOKABLE static Proto defaultProtocol(DockerContainer c); Q_INVOKABLE static amnezia::Proto defaultProtocol(amnezia::DockerContainer c);
Q_INVOKABLE static bool isSupportedByCurrentPlatform(DockerContainer c); Q_INVOKABLE static bool isSupportedByCurrentPlatform(amnezia::DockerContainer c);
Q_INVOKABLE static QStringList fixedPortsForContainer(amnezia::DockerContainer c);
}; };

View file

@ -253,12 +253,18 @@ ErrorCode ServerController::removeContainer(const ServerCredentials &credentials
genVarsForScript(credentials, container))); genVarsForScript(credentials, container)));
} }
ErrorCode ServerController::setupContainer(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config) ErrorCode ServerController::setupContainer(const ServerCredentials &credentials, DockerContainer container,
QJsonObject &config, bool isUpdate)
{ {
qDebug().noquote() << "ServerController::setupContainer" << ContainerProps::containerToString(container); qDebug().noquote() << "ServerController::setupContainer" << ContainerProps::containerToString(container);
//qDebug().noquote() << QJsonDocument(config).toJson(); //qDebug().noquote() << QJsonDocument(config).toJson();
ErrorCode e = ErrorCode::NoError; ErrorCode e = ErrorCode::NoError;
if (!isUpdate) {
e = isServerPortBusy(credentials, container, config);
if (e) return e;
}
e = installDockerWorker(credentials, container); e = installDockerWorker(credentials, container);
if (e) return e; if (e) return e;
qDebug().noquote() << "ServerController::setupContainer installDockerWorker finished"; qDebug().noquote() << "ServerController::setupContainer installDockerWorker finished";
@ -296,7 +302,7 @@ ErrorCode ServerController::updateContainer(const ServerCredentials &credentials
qDebug() << "ServerController::updateContainer for container" << container << "reinstall required is" << reinstallRequred; qDebug() << "ServerController::updateContainer for container" << container << "reinstall required is" << reinstallRequred;
if (reinstallRequred) { if (reinstallRequred) {
return setupContainer(credentials, container, newConfig); return setupContainer(credentials, container, newConfig, true);
} }
else { else {
ErrorCode e = configureContainerWorker(credentials, container, newConfig); ErrorCode e = configureContainerWorker(credentials, container, newConfig);
@ -661,3 +667,35 @@ QString ServerController::replaceVars(const QString &script, const Vars &vars)
//qDebug().noquote() << script; //qDebug().noquote() << script;
return s; return s;
} }
ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> proc) {
stdOut += data + "\n";
};
auto cbReadStdErr = [&](const QString &data, QSharedPointer<QSsh::SshRemoteProcess> ) {
stdOut += data + "\n";
};
const QString containerString = ProtocolProps::protoToString(ContainerProps::defaultProtocol(container));
const QJsonObject containerConfig = config.value(containerString).toObject();
QStringList fixedPorts = ContainerProps::fixedPortsForContainer(container);
QString port = containerConfig.value(config_key::port).toString();
QString transportProto = containerConfig.value(config_key::transport_proto).toString();
QString script = QString("sudo lsof -i -P -n | grep -E ':%1").arg(port);
for (auto &port : fixedPorts) {
script = script.append("|:%1").arg(port);
}
script = script.append("' | grep -i %1").arg(transportProto);
runScript(credentials,
replaceVars(script, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
if (!stdOut.isEmpty()) {
return ErrorCode::ServerPortAlreadyAllocatedError;
}
return ErrorCode::NoError;
}

View file

@ -32,9 +32,10 @@ public:
ErrorCode removeAllContainers(const ServerCredentials &credentials); ErrorCode removeAllContainers(const ServerCredentials &credentials);
ErrorCode removeContainer(const ServerCredentials &credentials, DockerContainer container); ErrorCode removeContainer(const ServerCredentials &credentials, DockerContainer container);
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config); ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container,
QJsonObject &config, bool isUpdate = false);
ErrorCode updateContainer(const ServerCredentials &credentials, DockerContainer container, ErrorCode updateContainer(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &oldConfig, QJsonObject &newConfig); const QJsonObject &oldConfig, QJsonObject &newConfig);
// create initial config - generate passwords, etc // create initial config - generate passwords, etc
QJsonObject createContainerInitialConfig(DockerContainer container, int port, TransportProto tp); QJsonObject createContainerInitialConfig(DockerContainer container, int port, TransportProto tp);
@ -77,6 +78,7 @@ private:
ErrorCode runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config); ErrorCode runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config);
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config); ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config);
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config = QJsonObject()); ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config = QJsonObject());
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config);
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
std::shared_ptr<VpnConfigurator> m_configurator; std::shared_ptr<VpnConfigurator> m_configurator;

View file

@ -15,7 +15,6 @@
#include "private/qandroidextras_p.h" #include "private/qandroidextras_p.h"
#include "ui/pages_logic/StartPageLogic.h" #include "ui/pages_logic/StartPageLogic.h"
#include "androidvpnactivity.h"
#include "androidutils.h" #include "androidutils.h"
namespace { namespace {
@ -262,6 +261,11 @@ void AndroidController::setVpnConfig(const QJsonObject &newVpnConfig)
m_vpnConfig = newVpnConfig; m_vpnConfig = newVpnConfig;
} }
void AndroidController::startQrReaderActivity()
{
AndroidVPNActivity::instance()->startQrCodeReader();
}
void AndroidController::scheduleStatusCheckSlot() void AndroidController::scheduleStatusCheckSlot()
{ {
QTimer::singleShot(1000, [this]() { QTimer::singleShot(1000, [this]() {

View file

@ -11,6 +11,7 @@
#include "ui/pages_logic/StartPageLogic.h" #include "ui/pages_logic/StartPageLogic.h"
#include "protocols/vpnprotocol.h" #include "protocols/vpnprotocol.h"
#include "androidvpnactivity.h"
using namespace amnezia; using namespace amnezia;
@ -42,6 +43,8 @@ public:
const QJsonObject &vpnConfig() const; const QJsonObject &vpnConfig() const;
void setVpnConfig(const QJsonObject &newVpnConfig); void setVpnConfig(const QJsonObject &newVpnConfig);
void startQrReaderActivity();
signals: signals:
void connectionStateChanged(VpnProtocol::VpnConnectionState state); void connectionStateChanged(VpnProtocol::VpnConnectionState state);
@ -50,8 +53,7 @@ signals:
// to true and the "connectionDate" should be set to the activation date if // to true and the "connectionDate" should be set to the activation date if
// known. // known.
// If "status" is set to false, the backend service is considered unavailable. // If "status" is set to false, the backend service is considered unavailable.
void initialized(bool status, bool connected, void initialized(bool status, bool connected, const QDateTime& connectionDate);
const QDateTime& connectionDate);
void statusUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4); void statusUpdated(QString totalRx, QString totalTx, QString endpoint, QString deviceIPv4);
void scheduleStatusCheckSignal(); void scheduleStatusCheckSignal();
@ -59,9 +61,6 @@ signals:
protected slots: protected slots:
void scheduleStatusCheckSlot(); void scheduleStatusCheckSlot();
protected:
private: private:
bool m_init = false; bool m_init = false;

View file

@ -22,12 +22,10 @@ AndroidVPNActivity::AndroidVPNActivity() {
AndroidUtils::runOnAndroidThreadAsync([]() { AndroidUtils::runOnAndroidThreadAsync([]() {
JNINativeMethod methods[]{ JNINativeMethod methods[]{
{"handleBackButton", "()Z", reinterpret_cast<bool*>(handleBackButton)}, {"handleBackButton", "()Z", reinterpret_cast<bool*>(handleBackButton)},
{"onServiceMessage", "(ILjava/lang/String;)V", {"onServiceMessage", "(ILjava/lang/String;)V", reinterpret_cast<void*>(onServiceMessage)},
reinterpret_cast<void*>(onServiceMessage)}, {"qtOnServiceConnected", "()V", reinterpret_cast<void*>(onServiceConnected)},
{"qtOnServiceConnected", "()V", {"qtOnServiceDisconnected", "()V", reinterpret_cast<void*>(onServiceDisconnected)},
reinterpret_cast<void*>(onServiceConnected)}, {"onActivityMessage", "(ILjava/lang/String;)V", reinterpret_cast<void*>(onAndroidVpnActivityMessage)}
{"qtOnServiceDisconnected", "()V",
reinterpret_cast<void*>(onServiceDisconnected)},
}; };
QJniObject javaClass(CLASSNAME); QJniObject javaClass(CLASSNAME);
@ -54,6 +52,11 @@ void AndroidVPNActivity::connectService() {
QJniObject::callStaticMethod<void>(CLASSNAME, "connectService", "()V"); QJniObject::callStaticMethod<void>(CLASSNAME, "connectService", "()V");
} }
void AndroidVPNActivity::startQrCodeReader()
{
QJniObject::callStaticMethod<void>(CLASSNAME, "startQrCodeReader", "()V");
}
// static // static
AndroidVPNActivity* AndroidVPNActivity::instance() { AndroidVPNActivity* AndroidVPNActivity::instance() {
if (s_instance == nullptr) { if (s_instance == nullptr) {
@ -121,6 +124,19 @@ void AndroidVPNActivity::handleServiceMessage(int code, const QString& data) {
} }
} }
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) { void AndroidVPNActivity::onServiceConnected(JNIEnv* env, jobject thiz) {
Q_UNUSED(env); Q_UNUSED(env);
Q_UNUSED(thiz); Q_UNUSED(thiz);
@ -134,3 +150,19 @@ void AndroidVPNActivity::onServiceDisconnected(JNIEnv* env, jobject thiz) {
emit AndroidVPNActivity::instance()->serviceDisconnected(); 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);
});
}

View file

@ -59,6 +59,11 @@ enum ServiceEvents {
}; };
typedef enum ServiceEvents ServiceEvents; typedef enum ServiceEvents ServiceEvents;
enum UIEvents {
QR_CODED_DECODED = 0,
};
typedef enum UIEvents UIEvents;
class AndroidVPNActivity : public QObject class AndroidVPNActivity : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -69,6 +74,7 @@ public:
static bool handleBackButton(JNIEnv* env, jobject thiz); static bool handleBackButton(JNIEnv* env, jobject thiz);
static void sendToService(ServiceAction type, const QString& data); static void sendToService(ServiceAction type, const QString& data);
static void connectService(); static void connectService();
static void startQrCodeReader();
signals: signals:
void serviceConnected(); void serviceConnected();
@ -80,6 +86,7 @@ signals:
void eventBackendLogs(const QString& data); void eventBackendLogs(const QString& data);
void eventActivationError(const QString& data); void eventActivationError(const QString& data);
void eventConfigImport(const QString& data); void eventConfigImport(const QString& data);
void eventQrCodeReceived(const QString& data);
private: private:
AndroidVPNActivity(); AndroidVPNActivity();
@ -87,7 +94,9 @@ private:
static void onServiceMessage(JNIEnv* env, jobject thiz, jint messageType, jstring body); static void onServiceMessage(JNIEnv* env, jobject thiz, jint messageType, jstring body);
static void onServiceConnected(JNIEnv* env, jobject thiz); static void onServiceConnected(JNIEnv* env, jobject thiz);
static void onServiceDisconnected(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 handleServiceMessage(int code, const QString& data);
void handleActivityMessage(int code, const QString& data);
}; };
#endif // ANDROIDVPNACTIVITY_H #endif // ANDROIDVPNACTIVITY_H

View file

@ -81,7 +81,6 @@
<file>ui/qml/Pages/PageSites.qml</file> <file>ui/qml/Pages/PageSites.qml</file>
<file>ui/qml/Pages/PageStart.qml</file> <file>ui/qml/Pages/PageStart.qml</file>
<file>ui/qml/Pages/PageVPN.qml</file> <file>ui/qml/Pages/PageVPN.qml</file>
<file>ui/qml/Pages/PageQrDecoder.qml</file>
<file>ui/qml/Pages/PageAbout.qml</file> <file>ui/qml/Pages/PageAbout.qml</file>
<file>ui/qml/Pages/PageQrDecoderIos.qml</file> <file>ui/qml/Pages/PageQrDecoderIos.qml</file>
<file>ui/qml/Pages/PageViewConfig.qml</file> <file>ui/qml/Pages/PageViewConfig.qml</file>

View file

@ -3,15 +3,70 @@
#include "ui/uilogic.h" #include "ui/uilogic.h"
#include "ui/pages_logic/StartPageLogic.h" #include "ui/pages_logic/StartPageLogic.h"
#ifdef Q_OS_ANDROID
#include <QJniEnvironment>
#include <QJniObject>
#include "../../platforms/android/androidutils.h"
#endif
using namespace amnezia; using namespace amnezia;
using namespace PageEnumNS; using namespace PageEnumNS;
namespace {
QrDecoderLogic* mInstance = nullptr;
constexpr auto CLASSNAME = "org.amnezia.vpn.qt.CameraActivity";
}
QrDecoderLogic::QrDecoderLogic(UiLogic *logic, QObject *parent): QrDecoderLogic::QrDecoderLogic(UiLogic *logic, QObject *parent):
PageLogicBase(logic, parent) PageLogicBase(logic, parent)
{ {
mInstance = this;
#if (defined(Q_OS_ANDROID))
AndroidUtils::runOnAndroidThreadAsync([]() {
JNINativeMethod methods[]{
{"passDataToDecoder", "(Ljava/lang/String;)V", reinterpret_cast<void*>(onNewDataChunk)},
};
QJniObject javaClass(CLASSNAME);
QJniEnvironment env;
jclass objectClass = env->GetObjectClass(javaClass.object<jobject>());
env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0]));
env->DeleteLocalRef(objectClass);
});
#endif
} }
void QrDecoderLogic::stopDecodingQr()
{
#if (defined(Q_OS_ANDROID))
QJniObject::callStaticMethod<void>(CLASSNAME, "stopQrCodeReader", "()V");
#endif
emit stopDecode();
}
#ifdef Q_OS_ANDROID
void QrDecoderLogic::onNewDataChunk(JNIEnv *env, jobject thiz, jstring data)
{
Q_UNUSED(thiz);
const char* buffer = env->GetStringUTFChars(data, nullptr);
if (!buffer) {
return;
}
QString parcelBody(buffer);
env->ReleaseStringUTFChars(data, buffer);
if (mInstance != nullptr) {
if (!mInstance->m_detectingEnabled) {
mInstance->onUpdatePage();
}
mInstance->onDetectedQrCode(parcelBody);
}
}
#endif
void QrDecoderLogic::onUpdatePage() void QrDecoderLogic::onUpdatePage()
{ {
m_chunks.clear(); m_chunks.clear();
@ -24,7 +79,6 @@ void QrDecoderLogic::onUpdatePage()
void QrDecoderLogic::onDetectedQrCode(const QString &code) void QrDecoderLogic::onDetectedQrCode(const QString &code)
{ {
//qDebug() << code; //qDebug() << code;
if (!detectingEnabled()) return; if (!detectingEnabled()) return;
// check if chunk received // check if chunk received
@ -32,12 +86,12 @@ void QrDecoderLogic::onDetectedQrCode(const QString &code)
QDataStream s(&ba, QIODevice::ReadOnly); QDataStream s(&ba, QIODevice::ReadOnly);
qint16 magic; s >> magic; qint16 magic; s >> magic;
if (magic == amnezia::qrMagicCode) { if (magic == amnezia::qrMagicCode) {
quint8 chunksCount; s >> chunksCount; quint8 chunksCount; s >> chunksCount;
if (totalChunksCount() != chunksCount) { if (totalChunksCount() != chunksCount) {
m_chunks.clear(); m_chunks.clear();
} }
set_totalChunksCount(chunksCount); set_totalChunksCount(chunksCount);
quint8 chunkId; s >> chunkId; quint8 chunkId; s >> chunkId;
@ -46,6 +100,7 @@ void QrDecoderLogic::onDetectedQrCode(const QString &code)
if (m_chunks.size() == totalChunksCount()) { if (m_chunks.size() == totalChunksCount()) {
QByteArray data; QByteArray data;
for (int i = 0; i < totalChunksCount(); ++i) { for (int i = 0; i < totalChunksCount(); ++i) {
data.append(m_chunks.value(i)); data.append(m_chunks.value(i));
} }
@ -53,21 +108,18 @@ void QrDecoderLogic::onDetectedQrCode(const QString &code)
bool ok = uiLogic()->pageLogic<StartPageLogic>()->importConnectionFromQr(data); bool ok = uiLogic()->pageLogic<StartPageLogic>()->importConnectionFromQr(data);
if (ok) { if (ok) {
set_detectingEnabled(false); set_detectingEnabled(false);
emit stopDecode(); stopDecodingQr();
} } else {
else {
m_chunks.clear(); m_chunks.clear();
set_totalChunksCount(0); set_totalChunksCount(0);
set_receivedChunksCount(0); set_receivedChunksCount(0);
} }
} }
} } else {
else {
bool ok = uiLogic()->pageLogic<StartPageLogic>()->importConnectionFromQr(ba); bool ok = uiLogic()->pageLogic<StartPageLogic>()->importConnectionFromQr(ba);
if (ok) { if (ok) {
set_detectingEnabled(false); set_detectingEnabled(false);
emit stopDecode(); stopDecodingQr();
} }
} }
} }

View file

@ -3,6 +3,10 @@
#include "PageLogicBase.h" #include "PageLogicBase.h"
#ifdef Q_OS_ANDROID
#include "jni.h"
#endif
class UiLogic; class UiLogic;
class QrDecoderLogic : public PageLogicBase class QrDecoderLogic : public PageLogicBase
@ -16,10 +20,17 @@ public:
Q_INVOKABLE void onUpdatePage() override; Q_INVOKABLE void onUpdatePage() override;
Q_INVOKABLE void onDetectedQrCode(const QString &code); Q_INVOKABLE void onDetectedQrCode(const QString &code);
#ifdef Q_OS_ANDROID
static void onNewDataChunk(JNIEnv *env, jobject thiz, jstring data);
#endif
public: public:
explicit QrDecoderLogic(UiLogic *uiLogic, QObject *parent = nullptr); explicit QrDecoderLogic(UiLogic *uiLogic, QObject *parent = nullptr);
~QrDecoderLogic() = default; ~QrDecoderLogic() = default;
private:
void stopDecodingQr();
signals: signals:
void startDecode(); void startDecode();
void stopDecode(); void stopDecode();

View file

@ -13,6 +13,7 @@
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include <QJniObject> #include <QJniObject>
#include "../../platforms/android/androidutils.h" #include "../../platforms/android/androidutils.h"
#include "../../platforms/android/android_controller.h"
#endif #endif
namespace { namespace {
@ -184,6 +185,13 @@ void StartPageLogic::onPushButtonImportOpenFile()
} }
} }
#ifdef Q_OS_ANDROID
void StartPageLogic::startQrDecoder()
{
AndroidController::instance()->startQrReaderActivity();
}
#endif
bool StartPageLogic::importConnection(const QJsonObject &profile) bool StartPageLogic::importConnection(const QJsonObject &profile)
{ {
ServerCredentials credentials; ServerCredentials credentials;

View file

@ -31,6 +31,10 @@ public:
Q_INVOKABLE void onPushButtonImport(); Q_INVOKABLE void onPushButtonImport();
Q_INVOKABLE void onPushButtonImportOpenFile(); Q_INVOKABLE void onPushButtonImportOpenFile();
#ifdef Q_OS_ANDROID
Q_INVOKABLE void startQrDecoder();
#endif
bool importConnection(const QJsonObject &profile); bool importConnection(const QJsonObject &profile);
bool importConnectionFromCode(QString code); bool importConnectionFromCode(QString code);
bool importConnectionFromQr(const QByteArray &data); bool importConnectionFromQr(const QByteArray &data);

View file

@ -1,164 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtMultimedia
import PageEnum 1.0
import QZXing 3.2
import "./"
import "../Controls"
import "../Config"
PageBase {
id: root
page: PageEnum.QrDecoder
logic: QrDecoderLogic
onDeactivated: {
console.debug("Stopping QR decoder")
loader.sourceComponent = undefined
}
BackButton {
}
Caption {
id: caption
text: qsTr("Import configuration")
}
Connections {
target: Qt.platform.os != "ios" ? QrDecoderLogic : null
function onStartDecode() {
console.debug("Starting QR decoder")
loader.sourceComponent = component
}
function onStopDecode() {
console.debug("Stopping QR decoder")
loader.sourceComponent = undefined
}
}
Loader {
id: loader
anchors.top: caption.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
Component {
id: component
Item {
anchors.fill: parent
Camera
{
id:camera
focus {
focusMode: CameraFocus.FocusContinuous
focusPointMode: CameraFocus.FocusPointAuto
}
}
VideoOutput
{
id: videoOutput
source: camera
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
autoOrientation: true
fillMode: VideoOutput.PreserveAspectFit
// filters: [ zxingFilter ]
Rectangle {
color: "black"
opacity: 0.5
width: videoOutput.contentRect.width * 0.15
height: videoOutput.contentRect.height
x: (videoOutput.width - videoOutput.contentRect.width)/2
anchors.verticalCenter: videoOutput.verticalCenter
}
Rectangle {
color: "black"
opacity: 0.5
width: videoOutput.contentRect.width * 0.15
height: videoOutput.contentRect.height
x: videoOutput.width/2 + videoOutput.contentRect.width/2 - videoOutput.contentRect.width * 0.15
anchors.verticalCenter: videoOutput.verticalCenter
}
Rectangle {
color: "black"
opacity: 0.5
width: videoOutput.contentRect.width * 0.7
height: videoOutput.contentRect.height * 0.15
x: (videoOutput.width - videoOutput.contentRect.width)/2 + videoOutput.contentRect.width * 0.15
y: (videoOutput.height - videoOutput.contentRect.height)/2
}
Rectangle {
color: "black"
opacity: 0.5
width: videoOutput.contentRect.width * 0.7
height: videoOutput.contentRect.height * 0.15
x: (videoOutput.width - videoOutput.contentRect.width)/2 + videoOutput.contentRect.width * 0.15
y: videoOutput.height/2 + videoOutput.contentRect.height/2 - videoOutput.contentRect.height * 0.15
}
LabelType {
width: parent.width
text: qsTr("Decoded QR chunks " + QrDecoderLogic.receivedChunksCount + "/" + QrDecoderLogic.totalChunksCount)
horizontalAlignment: Text.AlignLeft
visible: QrDecoderLogic.totalChunksCount > 0
anchors.horizontalCenter: videoOutput.horizontalCenter
y: videoOutput.height/2 + videoOutput.contentRect.height/2
}
}
QZXingFilter
{
id: zxingFilter
orientation: videoOutput.orientation
captureRect: {
// setup bindings
videoOutput.contentRect;
videoOutput.sourceRect;
return videoOutput.mapRectToSource(videoOutput.mapNormalizedRectToItem(Qt.rect(
0.15, 0.15, 0.7, 0.7 //0, 0, 1.0, 1.0
)));
}
decoder {
enabledDecoders: QZXing.DecoderFormat_QR_CODE
onTagFound: {
QrDecoderLogic.onDetectedQrCode(tag)
}
tryHarder: true
}
property int framesDecoded: 0
property real timePerFrameDecode: 0
onDecodingFinished:
{
timePerFrameDecode = (decodeTime + framesDecoded * timePerFrameDecode) / (framesDecoded + 1);
framesDecoded++;
if(succeeded)
console.log("frame finished: " + succeeded, decodeTime, timePerFrameDecode, framesDecoded);
}
}
}
}
}

View file

@ -154,7 +154,7 @@ PageBase {
if (Qt.platform.os === "ios") { if (Qt.platform.os === "ios") {
UiLogic.goToPage(PageEnum.QrDecoderIos) UiLogic.goToPage(PageEnum.QrDecoderIos)
} else { } else {
UiLogic.goToPage(PageEnum.QrDecoder) StartPageLogic.startQrDecoder()
} }
} }
enabled: StartPageLogic.pushButtonConnectEnabled enabled: StartPageLogic.pushButtonConnectEnabled

View file

@ -51,7 +51,7 @@ $QT_HOST_PATH/bin/androiddeployqt \
--gradle \ --gradle \
--release \ --release \
--input android-AmneziaVPN-deployment-settings.json \ --input android-AmneziaVPN-deployment-settings.json \
--android-platform android-31 --android-platform android-33
echo "............Copy apk.................." echo "............Copy apk.................."
cp $OUT_APP_DIR/android-build/build/outputs/apk/release/android-build-release-unsigned.apk \ cp $OUT_APP_DIR/android-build/build/outputs/apk/release/android-build-release-unsigned.apk \