mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -99,7 +99,9 @@ bool AndroidController::initialize()
|
||||
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
|
||||
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <typename Ret, typename ...Args>
|
||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "settingsController.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QOperatingSystemVersion>
|
||||
|
||||
#include "logger.h"
|
||||
#include "systemController.h"
|
||||
@@ -33,6 +34,17 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &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();
|
||||
|
||||
@@ -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<Settings> m_settings;
|
||||
|
||||
QString m_appVersion;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ PageType {
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -66,7 +66,7 @@ PageType {
|
||||
id: backButton
|
||||
objectName: "backButton"
|
||||
|
||||
Layout.topMargin: 20
|
||||
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
HeaderTypeWithButton {
|
||||
|
||||
@@ -61,7 +61,7 @@ PageType {
|
||||
width: root.width
|
||||
|
||||
BackButtonType {
|
||||
Layout.topMargin: 20
|
||||
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ PageType {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
|
||||
anchors.topMargin: 20
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user