diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1384eae42..3ae725637 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -470,7 +470,7 @@ jobs: env: ANDROID_BUILD_PLATFORM: android-36 - QT_VERSION: 6.9.3 + QT_VERSION: 6.8.3 QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 04001f63d..a0cddbb35 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -45,7 +45,7 @@ android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density |fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc" android:launchMode="singleInstance" - android:windowSoftInputMode="stateUnchanged|adjustResize" + android:windowSoftInputMode="adjustResize|stateUnchanged" android:enableOnBackInvokedCallback="false" android:exported="true"> @@ -215,4 +215,4 @@ - + \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 4260c05e8..4b684215a 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -35,6 +35,10 @@ import android.widget.Toast import androidx.annotation.MainThread import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import androidx.core.graphics.Insets +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE @@ -300,6 +304,11 @@ class AmneziaActivity : QtActivity() { isAppearanceLightStatusBars = false isAppearanceLightNavigationBars = false } + + // Workaround for Android 14 (API 34+) IME adjustResize bug + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setupImeInsetsListener() + } } else { window.apply { addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) @@ -308,6 +317,34 @@ class AmneziaActivity : QtActivity() { } } + private fun setupImeInsetsListener() { + ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets -> + val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime()) + + val imeHeight = if (imeVisible) imeInsets.bottom else 0 + + val density = resources.displayMetrics.density + val imeHeightDp = (imeHeight / density).toInt() + + // Also track system bars (navigation bar, status bar) changes + val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val navBarHeight = systemBarsInsets.bottom + val navBarHeightDp = (navBarHeight / density).toInt() + val statusBarHeight = systemBarsInsets.top + val statusBarHeightDp = (statusBarHeight / density).toInt() + + mainScope.launch { + qtInitialized.await() + QtAndroidController.onImeInsetsChanged(imeHeightDp) + QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp) + } + + // Return windowInsets instead of CONSUMED to allow proper handling + windowInsets + } + } + override fun onDestroy() { Log.d(TAG, "Destroy Amnezia activity") unregisterBroadcastReceiver(notificationStateReceiver) diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt index 4af138a28..b77e77d66 100644 --- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt +++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt @@ -28,4 +28,7 @@ object QtAndroidController { external fun onAuthResult(result: Boolean) external fun decodeQrCode(data: String): Boolean + + external fun onImeInsetsChanged(heightDp: Int) + external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int) } \ No newline at end of file diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index ee2e43e29..2d60ad849 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -99,7 +99,9 @@ bool AndroidController::initialize() {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)}, {"onAuthResult", "(Z)V", reinterpret_cast(onAuthResult)}, - {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)} + {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)}, + {"onImeInsetsChanged", "(I)V", reinterpret_cast(onImeInsetsChanged)}, + {"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast(onSystemBarsInsetsChanged)} }; QJniEnvironment env; @@ -536,3 +538,23 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data) return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data)); } +// static +void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + qDebug() << "Android IME insets changed: height =" << heightDp << "dp"; + emit AndroidController::instance()->imeInsetsChanged(heightDp); +} + +// static +void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + qDebug() << "Android system bars insets changed: nav bar =" << navBarHeightDp << "dp, status bar =" << statusBarHeightDp << "dp"; + emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp); +} + diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index b3b600602..a5a4b3d23 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -73,6 +73,8 @@ signals: void importConfigFromOutside(QString config); void initConnectionState(Vpn::ConnectionState state); void authenticationResult(bool result); + void imeInsetsChanged(int heightDp); + void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp); private: bool isWaitingStatus = true; @@ -101,6 +103,8 @@ private: static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); + static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp); + static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp); template static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 24b547b41..7eef384c5 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -1,6 +1,7 @@ #include "settingsController.h" #include +#include #include "logger.h" #include "systemController.h" @@ -33,6 +34,17 @@ SettingsController::SettingsController(const QSharedPointer &serve checkIfNeedDisableLogs(); #ifdef Q_OS_ANDROID connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged); + connect(AndroidController::instance(), &AndroidController::imeInsetsChanged, this, [this](int heightDp) { + m_imeHeight = heightDp; + emit imeHeightChanged(heightDp); + emit safeAreaBottomMarginChanged(); + }); + connect(AndroidController::instance(), &AndroidController::systemBarsInsetsChanged, this, [this](int navBarHeightDp, int statusBarHeightDp) { + m_cachedNavigationBarHeight = navBarHeightDp; + m_cachedStatusBarHeight = statusBarHeightDp; + emit safeAreaBottomMarginChanged(); + emit safeAreaTopMarginChanged(); + }); #endif m_isDevModeEnabled = m_settings->isDevGatewayEnv(); @@ -480,6 +492,10 @@ int SettingsController::getSafeAreaBottomMargin() { #ifdef Q_OS_ANDROID if (isEdgeToEdgeEnabled()) { + if (m_imeHeight > 0) { + return 0; + } + int height = getNavigationBarHeight(); int result = height > 0 ? height : 56; // fallback to 56 if system returns 0 return result; @@ -488,6 +504,11 @@ int SettingsController::getSafeAreaBottomMargin() return 0; } +int SettingsController::getImeHeight() +{ + return m_imeHeight; +} + bool SettingsController::isHomeAdLabelVisible() { return m_settings->isHomeAdLabelVisible(); diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index ad9b97afe..cc90de567 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -33,8 +33,9 @@ public: Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged) - Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin CONSTANT) - Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin CONSTANT) + Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged) + Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin NOTIFY safeAreaBottomMarginChanged) + Q_PROPERTY(int imeHeight READ getImeHeight NOTIFY imeHeightChanged) public slots: void toggleAmneziaDns(bool enable); @@ -103,6 +104,7 @@ public slots: int getNavigationBarHeight(); int getSafeAreaTopMargin(); int getSafeAreaBottomMargin(); + int getImeHeight(); bool isHomeAdLabelVisible(); void disableHomeAdLabel(); @@ -131,6 +133,10 @@ signals: void devModeEnabled(); void gatewayEndpointChanged(const QString &endpoint); void devGatewayEnvChanged(bool enabled); + + void imeHeightChanged(int height); + void safeAreaTopMarginChanged(); + void safeAreaBottomMarginChanged(); void isHomeAdLabelVisibleChanged(bool visible); void startMinimizedChanged(); @@ -144,6 +150,7 @@ private: mutable int m_cachedStatusBarHeight = -1; mutable int m_cachedNavigationBarHeight = -1; + int m_imeHeight = 0; std::shared_ptr m_settings; QString m_appVersion; diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index e67e36a15..65822afe8 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -49,6 +49,22 @@ Item { return drawerContent.state === stateName } + Connections { + target: Qt.application + + function onStateChanged() { + if (Qt.application.state !== Qt.ApplicationActive) { + if (dragArea.drag.active) { + dragArea.drag.target = null + dragArea.drag.target = drawerContent + } + if (isOpened && !isCollapsedStateActive()) { + root.closeTriggered() + } + } + } + } + Connections { target: PageController diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index 633b11cf6..540028159 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -74,6 +74,18 @@ Item { FocusController.nextKeyRightItem() } + Connections { + target: Qt.application + + function onStateChanged() { + if (Qt.application.state !== Qt.ApplicationActive) { + if (!menu.isClosed) { + menu.closeTriggered() + } + } + } + } + implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight diff --git a/client/ui/qml/Pages2/PageDeinstalling.qml b/client/ui/qml/Pages2/PageDeinstalling.qml index 03d26f8d9..24f497bd3 100644 --- a/client/ui/qml/Pages2/PageDeinstalling.qml +++ b/client/ui/qml/Pages2/PageDeinstalling.qml @@ -45,7 +45,7 @@ PageType { BaseHeaderType { Layout.fillWidth: true - Layout.topMargin: 20 + Layout.topMargin: 20 + SettingsController.safeAreaTopMargin Layout.leftMargin: 16 Layout.rightMargin: 16 diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml index 9f042fad4..81f44edbc 100644 --- a/client/ui/qml/Pages2/PageDevMenu.qml +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -22,7 +22,7 @@ PageType { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 20 + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin } ListViewType { diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 06bfb2430..1615d2761 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -20,6 +20,21 @@ import "../Components" PageType { id: root + Connections { + target: Qt.application + + function onStateChanged() { + if (Qt.application.state !== Qt.ApplicationActive) { + if (drawer.isOpened) { + drawer.closeTriggered() + } + if (homeSplitTunnelingDrawer.isOpened) { + homeSplitTunnelingDrawer.closeTriggered() + } + } + } + } + Connections { objectName: "pageControllerConnections" diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml index 2db21d495..47b4fb58c 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -22,7 +22,7 @@ PageType { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 20 + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin onActiveFocusChanged: { if(backButton.enabled && backButton.activeFocus) { diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index e4c57b5be..d2787f590 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -66,7 +66,7 @@ PageType { id: backButton objectName: "backButton" - Layout.topMargin: 20 + Layout.topMargin: 20 + SettingsController.safeAreaTopMargin } HeaderTypeWithButton { diff --git a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml index 38504edde..0a5ea6e34 100644 --- a/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml +++ b/client/ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml @@ -61,7 +61,7 @@ PageType { width: root.width BackButtonType { - Layout.topMargin: 20 + Layout.topMargin: 20 + SettingsController.safeAreaTopMargin } Label { diff --git a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml index 2e069cb7b..d7880dc56 100644 --- a/client/ui/qml/Pages2/PageSetupWizardInstalling.qml +++ b/client/ui/qml/Pages2/PageSetupWizardInstalling.qml @@ -103,7 +103,7 @@ PageType { BaseHeaderType { Layout.fillWidth: true - Layout.topMargin: 20 + Layout.topMargin: 20 + SettingsController.safeAreaTopMargin Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -147,6 +147,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 24 + Layout.bottomMargin: 24 + SettingsController.safeAreaBottomMargin Layout.leftMargin: 16 Layout.rightMargin: 16 diff --git a/client/ui/qml/Pages2/PageSetupWizardQrReader.qml b/client/ui/qml/Pages2/PageSetupWizardQrReader.qml index 83ead6f2d..320e0e206 100644 --- a/client/ui/qml/Pages2/PageSetupWizardQrReader.qml +++ b/client/ui/qml/Pages2/PageSetupWizardQrReader.qml @@ -20,7 +20,7 @@ PageType { anchors.left: parent.left anchors.top: parent.top - anchors.topMargin: 20 + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin } ParagraphTextType { diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 2d6790baf..82ff3b7c1 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -25,7 +25,7 @@ PageType { source: "qrc:/images/amneziaBigLogo.png" Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.topMargin: 32 + Layout.topMargin: 32 + SettingsController.safeAreaTopMargin Layout.preferredWidth: 360 Layout.preferredHeight: 287 } @@ -33,7 +33,7 @@ PageType { BasicButtonType { id: startButton Layout.fillWidth: true - Layout.bottomMargin: 48 + Layout.bottomMargin: 48 + SettingsController.safeAreaBottomMargin Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.alignment: Qt.AlignBottom diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index fbb04adea..ab4224fda 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -304,6 +304,9 @@ PageType { anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom + + // Also adjust TabBar position when keyboard appears (Android 14+ workaround) + anchors.bottomMargin: SettingsController.imeHeight topPadding: 8 bottomPadding: 8 + SettingsController.safeAreaBottomMargin