diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 561222f2..6e4abb1f 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -202,31 +202,6 @@ QList ExportController::getQrCodes() } void ExportController::saveFile() -{ - QString fileExtension = ".vpn"; - QString docDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); - QUrl fileName; - fileName = QFileDialog::getSaveFileUrl(nullptr, tr("Save AmneziaVPN config"), - QUrl::fromLocalFile(docDir + "/" + "amnezia_config"), "*" + fileExtension); - if (fileName.isEmpty()) - return; - if (!fileName.toString().endsWith(fileExtension)) { - fileName = QUrl(fileName.toString() + fileExtension); - } - if (fileName.isEmpty()) - return; - - QFile save(fileName.toLocalFile()); - - save.open(QIODevice::WriteOnly); - save.write(m_config.toUtf8()); - save.close(); - - QFileInfo fi(fileName.toLocalFile()); - QDesktopServices::openUrl(fi.absoluteDir().absolutePath()); -} - -void ExportController::shareFile() { #if defined Q_OS_IOS ext.replace("*", ""); @@ -253,6 +228,28 @@ void ExportController::shareFile() AndroidController::instance()->shareConfig(m_config, "amnezia_config"); return; #endif + + QString fileExtension = ".vpn"; + QString docDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QUrl fileName; + fileName = QFileDialog::getSaveFileUrl(nullptr, tr("Save AmneziaVPN config"), + QUrl::fromLocalFile(docDir + "/" + "amnezia_config"), "*" + fileExtension); + if (fileName.isEmpty()) + return; + if (!fileName.toString().endsWith(fileExtension)) { + fileName = QUrl(fileName.toString() + fileExtension); + } + if (fileName.isEmpty()) + return; + + QFile save(fileName.toLocalFile()); + + save.open(QIODevice::WriteOnly); + save.write(m_config.toUtf8()); + save.close(); + + QFileInfo fi(fileName.toLocalFile()); + QDesktopServices::openUrl(fi.absoluteDir().absolutePath()); } QList ExportController::generateQrCodeImageSeries(const QByteArray &data) diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index 7af2cd85..84575079 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -36,7 +36,6 @@ public slots: QList getQrCodes(); void saveFile(); - void shareFile(); signals: void generateConfig(int type); diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index 218f43cb..5711c3bd 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -41,6 +41,11 @@ namespace } return ConfigTypes::Amnezia; } + +#ifdef Q_OS_ANDROID + ImportController *mInstance = nullptr; + constexpr auto AndroidCameraActivity = "org.amnezia.vpn.qt.CameraActivity"; +#endif } // namespace ImportController::ImportController(const QSharedPointer &serversModel, @@ -49,6 +54,7 @@ ImportController::ImportController(const QSharedPointer &serversMo : QObject(parent), m_serversModel(serversModel), m_containersModel(containersModel), m_settings(settings) { #ifdef Q_OS_ANDROID + mInstance = this; // Set security screen for Android app AndroidUtils::runOnAndroidThreadSync([]() { QJniObject activity = AndroidUtils::getActivity(); @@ -58,6 +64,18 @@ ImportController::ImportController(const QSharedPointer &serversMo window.callMethod("addFlags", "(I)V", FLAG_SECURE); } }); + + AndroidUtils::runOnAndroidThreadAsync([]() { + JNINativeMethod methods[] { + { "passDataToDecoder", "(Ljava/lang/String;)V", reinterpret_cast(onNewQrCodeDataChunk) }, + }; + + QJniObject javaClass(AndroidCameraActivity); + QJniEnvironment env; + jclass objectClass = env->GetObjectClass(javaClass.object()); + env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0])); + env->DeleteLocalRef(objectClass); + }); #endif } @@ -93,11 +111,21 @@ void ImportController::extractConfigFromCode(QString code) m_configFileName = ""; } -void ImportController::extractConfigFromQr() +bool ImportController::extractConfigFromQr(const QByteArray &data) { -#ifdef Q_OS_ANDROID - AndroidController::instance()->startQrReaderActivity(); -#endif + QJsonObject dataObj = QJsonDocument::fromJson(data).object(); + if (!dataObj.isEmpty()) { + m_config = dataObj; + return true; + } + + QByteArray ba_uncompressed = qUncompress(data); + if (!ba_uncompressed.isEmpty()) { + m_config = QJsonDocument::fromJson(ba_uncompressed).object(); + return true; + } + + return false; } QString ImportController::getConfig() @@ -149,21 +177,6 @@ QJsonObject ImportController::extractAmneziaConfig(QString &data) return QJsonDocument::fromJson(ba).object(); } -// bool ImportController::importConnectionFromQr(const QByteArray &data) -//{ -// QJsonObject dataObj = QJsonDocument::fromJson(data).object(); -// if (!dataObj.isEmpty()) { -// return importConnection(dataObj); -// } - -// QByteArray ba_uncompressed = qUncompress(data); -// if (!ba_uncompressed.isEmpty()) { -// return importConnection(QJsonDocument::fromJson(ba_uncompressed).object()); -// } - -// return false; -//} - QJsonObject ImportController::extractOpenVpnConfig(const QString &data) { QJsonObject openVpnConfig; @@ -251,3 +264,90 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data) return config; } + +#ifdef Q_OS_ANDROID +void ImportController::startDecodingQr() +{ + AndroidController::instance()->startQrReaderActivity(); +} + +void ImportController::stopDecodingQr() +{ + QJniObject::callStaticMethod(AndroidCameraActivity, "stopQrCodeReader", "()V"); + emit qrDecodingFinished(); +} + +void ImportController::onNewQrCodeDataChunk(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_isQrCodeProcessed) { + mInstance->m_qrCodeChunks.clear(); + mInstance->m_isQrCodeProcessed = true; + mInstance->m_totalQrCodeChunksCount = 0; + mInstance->m_receivedQrCodeChunksCount = 0; + } + mInstance->parseQrCodeChunk(parcelBody); + } +} + +void ImportController::parseQrCodeChunk(const QString &code) +{ + // qDebug() << code; + if (!m_isQrCodeProcessed) + return; + + // check if chunk received + QByteArray ba = QByteArray::fromBase64(code.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + QDataStream s(&ba, QIODevice::ReadOnly); + qint16 magic; + s >> magic; + + if (magic == amnezia::qrMagicCode) { + quint8 chunksCount; + s >> chunksCount; + if (m_totalQrCodeChunksCount != chunksCount) { + m_qrCodeChunks.clear(); + } + + m_totalQrCodeChunksCount = chunksCount; + + quint8 chunkId; + s >> chunkId; + s >> m_qrCodeChunks[chunkId]; + m_receivedQrCodeChunksCount = m_qrCodeChunks.size(); + + if (m_qrCodeChunks.size() == m_totalQrCodeChunksCount) { + QByteArray data; + + for (int i = 0; i < m_totalQrCodeChunksCount; ++i) { + data.append(m_qrCodeChunks.value(i)); + } + + bool ok = extractConfigFromQr(data); + if (ok) { + m_isQrCodeProcessed = false; + stopDecodingQr(); + } else { + m_qrCodeChunks.clear(); + m_totalQrCodeChunksCount = 0; + m_receivedQrCodeChunksCount = 0; + } + } + } else { + bool ok = extractConfigFromQr(ba); + if (ok) { + m_isQrCodeProcessed = false; + stopDecodingQr(); + } + } +} +#endif diff --git a/client/ui/controllers/importController.h b/client/ui/controllers/importController.h index 273b12a5..3bdb2252 100644 --- a/client/ui/controllers/importController.h +++ b/client/ui/controllers/importController.h @@ -7,6 +7,9 @@ #include "core/defs.h" #include "ui/models/containers_model.h" #include "ui/models/servers_model.h" +#ifdef Q_OS_ANDROID + #include "jni.h" +#endif class ImportController : public QObject { @@ -21,25 +24,44 @@ public slots: void extractConfigFromFile(); void extractConfigFromData(QString &data); void extractConfigFromCode(QString code); - void extractConfigFromQr(); + bool extractConfigFromQr(const QByteArray &data); QString getConfig(); QString getConfigFileName(); +#if defined Q_OS_ANDROID + void startDecodingQr(); +#endif + signals: void importFinished(); void importErrorOccurred(QString errorMessage); + void qrDecodingFinished(); + private: QJsonObject extractAmneziaConfig(QString &data); QJsonObject extractOpenVpnConfig(const QString &data); QJsonObject extractWireGuardConfig(const QString &data); +#if defined Q_OS_ANDROID + void stopDecodingQr(); + static void onNewQrCodeDataChunk(JNIEnv *env, jobject thiz, jstring data); + void parseQrCodeChunk(const QString &code); +#endif + QSharedPointer m_serversModel; QSharedPointer m_containersModel; std::shared_ptr m_settings; QJsonObject m_config; QString m_configFileName; + +#if defined Q_OS_ANDROID + QMap m_qrCodeChunks; + bool m_isQrCodeProcessed; + int m_totalQrCodeChunksCount; + int m_receivedQrCodeChunksCount; +#endif }; #endif // IMPORTCONTROLLER_H diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index 5abeb77f..36d5ba7c 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -1,10 +1,28 @@ #include "pageController.h" #include +#ifdef Q_OS_ANDROID + #include "../../platforms/android/androidutils.h" + #include +#endif PageController::PageController(const QSharedPointer &serversModel, QObject *parent) : QObject(parent), m_serversModel(serversModel) { +#ifdef Q_OS_ANDROID + // Change color of navigation and status bar's + auto initialPageNavigationBarColor = getInitialPageNavigationBarColor(); + AndroidUtils::runOnAndroidThreadSync([&initialPageNavigationBarColor]() { + QJniObject activity = AndroidUtils::getActivity(); + QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); + if (window.isValid()) { + window.callMethod("addFlags", "(I)V", 0x80000000); + window.callMethod("clearFlags", "(I)V", 0x04000000); + window.callMethod("setStatusBarColor", "(I)V", 0xFF0E0E11); + window.callMethod("setNavigationBarColor", "(I)V", initialPageNavigationBarColor); + } + }); +#endif } QString PageController::getInitialPage() @@ -47,3 +65,26 @@ void PageController::keyPressEvent(Qt::Key key) default: return; } } + +unsigned int PageController::getInitialPageNavigationBarColor() +{ + if (m_serversModel->getServersCount()) { + return 0xFF1C1D21; + } else { + return 0xFF0E0E11; + } +} + +void PageController::updateNavigationBarColor(const int color) +{ +#ifdef Q_OS_ANDROID + // Change color of navigation bar + AndroidUtils::runOnAndroidThreadSync([&color]() { + QJniObject activity = AndroidUtils::getActivity(); + QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); + if (window.isValid()) { + window.callMethod("setNavigationBarColor", "(I)V", color); + } + }); +#endif +} diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index e8452b45..8087d0fe 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -71,6 +71,9 @@ public slots: void closeWindow(); void keyPressEvent(Qt::Key key); + unsigned int getInitialPageNavigationBarColor(); + void updateNavigationBarColor(const int color); + signals: void goToPageHome(); void goToPageSettings(); diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 692289a8..f133f27a 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -54,10 +54,10 @@ DrawerType { Layout.fillWidth: true Layout.topMargin: 16 - text: Qt.platform.os === "android" ? qsTr("Share") : qsTr("Save connection code") + text: qsTr("Share") onClicked: { - Qt.platform.os === "android" ? ExportController.shareFile() : ExportController.saveFile() + ExportController.saveFile() } } diff --git a/client/ui/qml/Controls2/DrawerType.qml b/client/ui/qml/Controls2/DrawerType.qml index e3c9d588..97fbf034 100644 --- a/client/ui/qml/Controls2/DrawerType.qml +++ b/client/ui/qml/Controls2/DrawerType.qml @@ -12,6 +12,7 @@ Drawer { velocity: 4 } } + exit: Transition { SmoothedAnimation { velocity: 4 @@ -31,4 +32,17 @@ Drawer { Overlay.modal: Rectangle { color: Qt.rgba(14/255, 14/255, 17/255, 0.8) } + + onAboutToShow: { + if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(0xFF1C1D21) + } + } + + onClosed: { + var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor() + if (initialPageNavigationBarColor !== 0xFF1C1D21) { + PageController.updateNavigationBarColor(initialPageNavigationBarColor) + } + } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index ae24c942..2d6b249d 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -13,6 +13,14 @@ import "../Config" PageType { id: root + Connections { + target: ImportController + + function onQrDecodingFinished() { + goToPage(PageEnum.PageSetupWizardViewConfig) + } + } + FlickableType { id: fl anchors.top: parent.top @@ -77,7 +85,7 @@ It's okay if a friend passed the code.") leftImageSource: "qrc:/images/controls/qr-code.svg" clickedFunction: function() { - ImportController.extractConfigFromQr() + ImportController.startDecodingQr() // goToPage(PageEnum.PageSetupWizardQrReader) } } diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index a0fdf7be..92c95108 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -47,8 +47,7 @@ PageType { } else if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSettings)) { goToPage(PageEnum.PageSettingsServersList, false) } else { - var pagePath = PageController.getPagePath(PageEnum.PageStart) - stackView.replace(pagePath, { "objectName" : pagePath }) + PageController.replaceStartPage() } if (isInstalledContainerFound) { diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index e1b93302..372a5cde 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -30,8 +30,7 @@ PageType { } else if (stackView.currentItem.objectName === PageController.getPagePath(PageEnum.PageSettings)) { goToPage(PageEnum.PageSettingsServersList, false) } else { - var pagePath = PageController.getPagePath(PageEnum.PageStart) - stackView.replace(pagePath, { "objectName" : pagePath }) + PageController.replaceStartPage() } } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 5d49abf7..f522bace 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Shapes import PageEnum 1.0 @@ -81,14 +82,27 @@ PageType { anchors.bottom: parent.bottom topPadding: 8 - bottomPadding: 8//34 + bottomPadding: 8 leftPadding: shareTabButton.visible ? 96 : 128 rightPadding: shareTabButton.visible ? 96 : 128 - background: Rectangle { - border.width: 1 - border.color: "#2C2D30" - color: "#1C1D21" + background: Shape { + width: parent.width + height: parent.height + + ShapePath { + startX: 0 + startY: 0 + + PathLine { x: width; y: 0 } + PathLine { x: width; y: height - 1 } + PathLine { x: 0; y: height - 1 } + PathLine { x: 0; y: 0 } + + strokeWidth: 1 + strokeColor: "#2C2D30" + fillColor: "#1C1D21" + } } TabImageButtonType { diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index d0f9880d..0b89d840 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -50,6 +50,7 @@ Window { while (rootStackView.depth > 1) { rootStackView.pop() } + PageController.updateNavigationBarColor(PageController.getInitialPageNavigationBarColor()) rootStackView.replace(pagePath, { "objectName" : pagePath }) }