Merge pull request #10178 from f321x/android15_edge_to_edge_compatibility
android: make app compatible with edge-to-edge layout
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -21,6 +21,7 @@ ElDialog {
|
||||
anchors.centerIn: parent
|
||||
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
width: rootLayout.width
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ ElDialog {
|
||||
|
||||
property string selectedPubkey
|
||||
|
||||
needsSystemBarPadding: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
padding: 0
|
||||
|
||||
@@ -25,6 +25,7 @@ ElDialog {
|
||||
anchors.centerIn: parent
|
||||
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
@@ -20,6 +20,7 @@ ElDialog {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 4/5
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
ColumnLayout {
|
||||
id: rootLayout
|
||||
|
||||
@@ -25,6 +25,7 @@ ElDialog {
|
||||
focus: true
|
||||
closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose
|
||||
allowClose: canCancel
|
||||
needsSystemBarPadding: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ ElDialog {
|
||||
property bool isLightning: false
|
||||
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ ElDialog {
|
||||
anchors.centerIn: parent
|
||||
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
width: rootPane.width
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ ElDialog {
|
||||
anchors.centerIn: parent
|
||||
|
||||
padding: 0
|
||||
needsSystemBarPadding: false
|
||||
|
||||
width: rootLayout.width
|
||||
|
||||
|
||||
Reference in New Issue
Block a user