diff --git a/contrib/android/buildozer_qml.spec b/contrib/android/buildozer_qml.spec index a0e4e6709..e19bcac5a 100644 --- a/contrib/android/buildozer_qml.spec +++ b/contrib/android/buildozer_qml.spec @@ -108,7 +108,7 @@ android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIO android.api = 31 # (int) Android targetSdkVersion -android.target_sdk_version = 34 +android.target_sdk_version = 35 # (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. android.minapi = 23 diff --git a/electrum/gui/qml/components/BIP39RecoveryDialog.qml b/electrum/gui/qml/components/BIP39RecoveryDialog.qml index cda047a78..6910d3de8 100644 --- a/electrum/gui/qml/components/BIP39RecoveryDialog.qml +++ b/electrum/gui/qml/components/BIP39RecoveryDialog.qml @@ -18,6 +18,8 @@ ElDialog { property string derivationPath property string scriptType + needsSystemBarPadding: false + z: 1 // raise z so it also covers wizard dialog anchors.centerIn: parent diff --git a/electrum/gui/qml/components/ExceptionDialog.qml b/electrum/gui/qml/components/ExceptionDialog.qml index e4159232a..9bc3a7b5b 100644 --- a/electrum/gui/qml/components/ExceptionDialog.qml +++ b/electrum/gui/qml/components/ExceptionDialog.qml @@ -18,10 +18,14 @@ ElDialog width: parent.width height: parent.height z: 1000 // assure topmost of all other dialogs. note: child popups need even higher! + // disable padding in ElDialog as it is overwritten here and shows no effect, this dialog needs padding though + needsSystemBarPadding: false header: null ColumnLayout { + anchors.topMargin: app.statusBarHeight // edge-to-edge layout padding + anchors.bottomMargin: app.navigationBarHeight anchors.fill: parent enabled: !_sending diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index ce6b85f5f..338dc08d8 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -16,6 +16,7 @@ ElDialog { property InvoiceParser invoiceParser padding: 0 + needsSystemBarPadding: false property bool commentValid: comment.text.length <= invoiceParser.lnurlData['comment_allowed'] property bool amountValid: amountBtc.textAsSats.satsInt >= parseInt(invoiceParser.lnurlData['min_sendable_sat']) diff --git a/electrum/gui/qml/components/LoadingWalletDialog.qml b/electrum/gui/qml/components/LoadingWalletDialog.qml index f40327f1d..2969cdc4d 100644 --- a/electrum/gui/qml/components/LoadingWalletDialog.qml +++ b/electrum/gui/qml/components/LoadingWalletDialog.qml @@ -18,6 +18,7 @@ ElDialog { x: Math.floor((parent.width - implicitWidth) / 2) y: Math.floor((parent.height - implicitHeight) / 2) // anchors.centerIn: parent // this strangely pixelates the spinner + needsSystemBarPadding: false function open() { showTimer.start() @@ -31,6 +32,10 @@ ElDialog { running: Daemon.loading } + + Item { + Layout.preferredHeight: 20 + } } Connections { diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index e80d4bb88..a2211ff75 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -21,6 +21,7 @@ ElDialog { anchors.centerIn: parent padding: 0 + needsSystemBarPadding: false width: rootLayout.width diff --git a/electrum/gui/qml/components/NostrSwapServersDialog.qml b/electrum/gui/qml/components/NostrSwapServersDialog.qml index 384789e0f..3330d782c 100644 --- a/electrum/gui/qml/components/NostrSwapServersDialog.qml +++ b/electrum/gui/qml/components/NostrSwapServersDialog.qml @@ -15,6 +15,8 @@ ElDialog { property string selectedPubkey + needsSystemBarPadding: false + anchors.centerIn: parent padding: 0 diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index 59319bcff..b6209dec5 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -25,6 +25,7 @@ ElDialog { anchors.centerIn: parent padding: 0 + needsSystemBarPadding: false ColumnLayout { spacing: 0 diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index 442f44a19..71a43d9a4 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -20,6 +20,7 @@ ElDialog { anchors.centerIn: parent width: parent.width * 4/5 padding: 0 + needsSystemBarPadding: false ColumnLayout { id: rootLayout diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index 261a37574..1f222c0a8 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -25,6 +25,7 @@ ElDialog { focus: true closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose allowClose: canCancel + needsSystemBarPadding: false anchors.centerIn: parent diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 136af00a3..ab000913d 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -20,6 +20,7 @@ ElDialog { property bool isLightning: false padding: 0 + needsSystemBarPadding: false ColumnLayout { width: parent.width diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 1a3abd4f1..74bd44c21 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -9,11 +9,16 @@ Dialog { property bool allowClose: true property string iconSource property bool resizeWithKeyboard: true + // inheriting classes can set needsSystemBarPadding this false to disable padding + property bool needsSystemBarPadding: true property bool _result: false // workaround: remember opened state, to inhibit closed -> closed event property bool _wasOpened: false + // Add bottom padding for Android navigation bar if needed + bottomPadding: needsSystemBarPadding ? app.navigationBarHeight : 0 + // called to finally close dialog after checks by onClosing handler in main.qml function doClose() { doReject() @@ -65,6 +70,13 @@ Dialog { header: ColumnLayout { spacing: 0 + // Add top padding for status bar on Android when using edge-to-edge + Item { + visible: needsSystemBarPadding && app.statusBarHeight > 0 + Layout.fillWidth: true + Layout.preferredHeight: app.statusBarHeight + } + RowLayout { spacing: 0 diff --git a/electrum/gui/qml/components/controls/HelpDialog.qml b/electrum/gui/qml/components/controls/HelpDialog.qml index f11a0d138..8d1b87d86 100644 --- a/electrum/gui/qml/components/controls/HelpDialog.qml +++ b/electrum/gui/qml/components/controls/HelpDialog.qml @@ -16,6 +16,7 @@ ElDialog { anchors.centerIn: parent padding: 0 + needsSystemBarPadding: false width: rootPane.width diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 826d7a27d..cfdbf4eeb 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -19,6 +19,9 @@ ApplicationWindow visible: false // initial value + readonly property int statusBarHeight: AppController ? AppController.getStatusBarHeight() : 0 + readonly property int navigationBarHeight: AppController ? AppController.getNavigationBarHeight() : 0 + // dimensions ignored on android width: 480 height: 800 @@ -117,6 +120,9 @@ ApplicationWindow header: ToolBar { id: toolbar + + // Add top margin for status bar on Android when using edge-to-edge + topPadding: app.statusBarHeight background: Rectangle { implicitHeight: 48 @@ -133,7 +139,7 @@ ApplicationWindow spacing: 0 anchors.left: parent.left anchors.right: parent.right - height: toolbar.height + height: toolbar.availableHeight RowLayout { id: toolbarTopLayout @@ -277,6 +283,13 @@ ApplicationWindow } } } + + // Add bottom padding for navigation bar on Android when UI is edge-to-edge + Item { + visible: app.navigationBarHeight > 0 + Layout.fillWidth: true + Layout.preferredHeight: app.navigationBarHeight + } } Timer { diff --git a/electrum/gui/qml/java_classes/org/electrum/qr/SimpleScannerActivity.java b/electrum/gui/qml/java_classes/org/electrum/qr/SimpleScannerActivity.java index 58eeeb81e..d6db0973e 100644 --- a/electrum/gui/qml/java_classes/org/electrum/qr/SimpleScannerActivity.java +++ b/electrum/gui/qml/java_classes/org/electrum/qr/SimpleScannerActivity.java @@ -2,6 +2,7 @@ package org.electrum.qr; import android.app.Activity; import android.os.Bundle; +import android.os.Build; import android.util.Log; import android.content.Intent; import android.Manifest; @@ -12,6 +13,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; @@ -68,6 +70,7 @@ public class SimpleScannerActivity extends Activity { } } }); + setupEdgeToEdge(); } @Override @@ -156,4 +159,51 @@ public class SimpleScannerActivity extends Activity { } } + private boolean enforcesEdgeToEdge() { + // if true the UI needs to be padded to be e2e compatible + return Build.VERSION.SDK_INT >= 35; + } + + private void setupEdgeToEdge() { + if (!enforcesEdgeToEdge()) { + return; + } + + // Get the root view and set up insets listener + getWindow().getDecorView().setOnApplyWindowInsetsListener((v, insets) -> { + android.graphics.Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); + + // Apply padding to content frame to keep scanner focus area centered + ViewGroup contentFrame = findViewById(R.id.content_frame); + if (contentFrame != null) { + contentFrame.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ); + } + + // Apply top padding to hint text for status bar + TextView hintTextView = findViewById(R.id.hint); + if (hintTextView != null) { + hintTextView.setPadding( + hintTextView.getPaddingLeft(), + systemBars.top, + hintTextView.getPaddingRight(), + hintTextView.getPaddingBottom() + ); + } + + // Apply bottom margin to paste button for navigation bar + Button pasteButton = findViewById(R.id.paste_btn); + if (pasteButton != null) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) pasteButton.getLayoutParams(); + params.bottomMargin = systemBars.bottom; + pasteButton.setLayoutParams(params); + } + + return insets; + }); + } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 45e9feaf2..3232cf6ca 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -22,6 +22,7 @@ from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue from electrum.network import Network from electrum.plugin import run_hook from electrum.gui.common_qt.util import get_font_id +from electrum.util import profiler from .qeconfig import QEConfig from .qedaemon import QEDaemon @@ -60,6 +61,7 @@ if 'ANDROID_DATA' in os.environ: jString = autoclass('java.lang.String') jIntent = autoclass('android.content.Intent') jview = jpythonActivity.getWindow().getDecorView() + systemSdkVersion = autoclass('android.os.Build$VERSION').SDK_INT notification = None @@ -414,6 +416,54 @@ class QEAppController(BaseCrashReporter, QObject): self._secureWindow = secure self.secureWindowChanged.emit() + @pyqtSlot(result=bool) + def enforcesEdgeToEdge(self) -> bool: + if not self.isAndroid(): + return False + return bool(systemSdkVersion >= 35) + + @profiler(min_threshold=0.02) + def _getSystemBarHeight(self, bar_type: str) -> int: + if not self.enforcesEdgeToEdge(): + return 0 + assert systemSdkVersion >= 30, \ + f"Android WindowInsets unavailable on {systemSdkVersion=}" + try: + root_insets = jview.getRootWindowInsets() + window_insets_type = autoclass('android.view.WindowInsets$Type') + + if bar_type == 'status': + ins = root_insets.getInsets(window_insets_type.statusBars()) + elif bar_type == 'navigation': + ins = root_insets.getInsets(window_insets_type.navigationBars()) + else: + raise ValueError(f"Invalid bar_type: {bar_type}") + + # Get the display metrics to convert pixels to dp + display_metrics = jpythonActivity.getResources().getDisplayMetrics() + density = display_metrics.density + + height = int(max(ins.bottom, ins.right, ins.left, ins.top, 0)) + if not height > 0: + return 0 + + # Convert from pixels to dp for QML + height_dp = int(height / density) + + self.logger.debug(f"_getSystemBarHeight: {height=}, {height_dp=}, {bar_type=}") + return max(0, height_dp) + except Exception as e: + self.logger.debug(f"{bar_type} fallback due to: {e!r}") + return 0 + + @pyqtSlot(result=int) + def getStatusBarHeight(self) -> int: + return self._getSystemBarHeight('status') + + @pyqtSlot(result=int) + def getNavigationBarHeight(self) -> int: + return self._getSystemBarHeight('navigation') + class ElectrumQmlApplication(QGuiApplication): diff --git a/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml index 286324ff8..bb59d3fbc 100644 --- a/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml +++ b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml @@ -26,6 +26,7 @@ ElDialog { anchors.centerIn: parent padding: 0 + needsSystemBarPadding: false width: rootLayout.width