1
0

android: pass on QR binary scan result data as well

qml: add signals for QEQRScanner and fallbacks, add QEBytes container type
so we can pass along byte arrays between QML and python, port qr scan
occurrences to new signals.
This commit is contained in:
Sander van Grieken
2025-07-22 14:18:43 +02:00
parent da18c975a2
commit aaed64c45a
14 changed files with 86 additions and 69 deletions

View File

@@ -91,11 +91,11 @@ ElDialog {
? qsTr('Scan another address')
: qsTr('Scan another private key')
})
dialog.onFound.connect(function() {
if (verify(dialog.scanData)) {
dialog.onFoundText.connect(function(data) {
if (verify(data)) {
if (import_ta.text != '')
import_ta.text = import_ta.text + ',\n'
import_ta.text = import_ta.text + dialog.scanData
import_ta.text = import_ta.text + data
}
dialog.close()
})

View File

@@ -62,8 +62,8 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a channel backup')
})
dialog.onFound.connect(function() {
channelbackup_ta.text = dialog.scanData
dialog.onFoundText.connect(function(data) {
channelbackup_ta.text = data
dialog.close()
})
dialog.open()

View File

@@ -124,9 +124,9 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a node-id or a connect string')
})
dialog.onFound.connect(function() {
if (channelopener.validateConnectString(dialog.scanData)) {
channelopener.connectStr = dialog.scanData
dialog.onFoundText.connect(function(data) {
if (channelopener.validateConnectString(data)) {
channelopener.connectStr = data
node.text = channelopener.connectStr
} else {
var errdialog = app.messageDialog.createObject(app, {

View File

@@ -2,17 +2,19 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.electrum
import "controls"
// currently not used on android, kept for future use when qt6 camera stops crashing
ElDialog {
id: scanDialog
property string scanData
property string error
property string hint
signal found
signal foundText(data: string)
signal foundBinary(data: Bytes)
width: parent.width
height: parent.height
@@ -35,9 +37,8 @@ ElDialog {
Layout.fillWidth: true
Layout.fillHeight: true
hint: scanDialog.hint
onFound: {
scanDialog.scanData = scanData
scanDialog.found()
onFoundText: (data) => {
scanDialog.foundText(data)
}
}

View File

@@ -59,7 +59,10 @@ ElDialog {
hint: Daemon.currentWallet.isLightning
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
onFound: dialog.dispatch(scanData)
onFoundText: (data) => {
dialog.dispatch(data)
}
}
ButtonContainer {

View File

@@ -117,9 +117,9 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a private key')
})
dialog.onFound.connect(function() {
if (verifyPrivateKey(dialog.scanData))
addPrivateKey(dialog.scanData)
dialog.onFoundText.connect(function(data) {
if (verifyPrivateKey(data))
addPrivateKey(data)
dialog.close()
})
dialog.open()

View File

@@ -47,8 +47,7 @@ Item {
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
})
scanner.onFound.connect(function() {
var data = scanner.scanData
scanner.onFoundText.connect(function(data) {
data = data.trim()
if (bitcoin.isRawTx(data)) {
app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })

View File

@@ -10,14 +10,12 @@ Item {
property bool active: false
property string url
property string scanData
property string hint
signal found
signal foundText(data: string)
function restart() {
console.log('qrscan.restart')
scanData = ''
qr.reset()
start()
}
@@ -153,8 +151,7 @@ Item {
function onDataChanged() {
console.log('QR DATA: ' + qr.data)
scanner.active = false
scanner.scanData = qr.data
scanner.found()
scanner.foundText(qr.data)
}
}

View File

@@ -166,9 +166,9 @@ WizardComponent {
? qsTr('Scan a cosigner master public key')
: qsTr('Scan a master key')
})
dialog.onFound.connect(function() {
if (verifyMasterKey(dialog.scanData))
masterkey_ta.text = dialog.scanData
dialog.onFoundText.connect(function(data) {
if (verifyMasterKey(data))
masterkey_ta.text = data
else
masterkey_ta.text = ''
dialog.close()

View File

@@ -76,11 +76,11 @@ WizardComponent {
? qsTr('Scan another private key')
: qsTr('Scan a private key or an address')
})
dialog.onFound.connect(function() {
if (verify(dialog.scanData)) {
dialog.onFoundText.connect(function(data) {
if (verify(data)) {
if (import_ta.text != '')
import_ta.text = import_ta.text + '\n'
import_ta.text = import_ta.text + dialog.scanData
import_ta.text = import_ta.text + data
}
dialog.close()
})

View File

@@ -21,6 +21,8 @@ import androidx.core.app.ActivityCompat;
import java.util.Arrays;
import de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView;
import de.markusfisch.android.zxingcpp.ZxingCpp.Result;
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType;
import org.electrum.electrum.res.R; // package set in build.gradle
@@ -29,7 +31,7 @@ public class SimpleScannerActivity extends Activity {
private static final int MY_PERMISSIONS_CAMERA = 1002;
private BarcodeScannerView mScannerView = null;
final String TAG = "org.electrum.SimpleScannerActivity";
final String TAG = "org.electrum.qr.SimpleScannerActivity";
private boolean mAlreadyRequestedPermissions = false;
@@ -60,7 +62,7 @@ public class SimpleScannerActivity extends Activity {
Toast.makeText(SimpleScannerActivity.this, "Clipboard contents too large.", Toast.LENGTH_SHORT).show();
return;
}
SimpleScannerActivity.this.setResultAndClose(clipboardText);
SimpleScannerActivity.this.setResultAndClose(null, clipboardText);
} else {
Toast.makeText(SimpleScannerActivity.this, "Clipboard is empty.", Toast.LENGTH_SHORT).show();
}
@@ -96,7 +98,7 @@ public class SimpleScannerActivity extends Activity {
contentFrame.addView(mScannerView);
mScannerView.setOnBarcodeListener(result -> {
// Handle the scan result
this.setResultAndClose(result.getText());
this.setResultAndClose(result, null);
// Return false to stop scanning after first result
return false;
});
@@ -104,9 +106,22 @@ public class SimpleScannerActivity extends Activity {
mScannerView.openAsync(); // Start camera on resume
}
private void setResultAndClose(String resultText) {
private void setResultAndClose(Result scanResult, String textOnly) {
Intent resultIntent = new Intent();
resultIntent.putExtra("text", resultText);
if (textOnly != null) {
Log.v(TAG, "clipboard contentType TEXT");
resultIntent.putExtra("text", textOnly);
} else if (scanResult != null) {
if (scanResult.getContentType() == ContentType.TEXT) {
Log.v(TAG, "scanResult contentType TEXT");
resultIntent.putExtra("text", scanResult.getText());
} else if (scanResult.getContentType() == ContentType.BINARY) {
Log.v(TAG, "scanResult contentType BINARY");
resultIntent.putExtra("binary", scanResult.getRawBytes());
} else {
Log.v(TAG, "scanresult contenttype unknown");
}
}
setResult(Activity.RESULT_OK, resultIntent);
this.finish();
}

View File

@@ -34,7 +34,7 @@ from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
from .qeinvoice import QEInvoice, QEInvoiceParser
from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount
from .qetypes import QEAmount, QEBytes
from .qeaddressdetails import QEAddressDetails
from .qetxdetails import QETxDetails
from .qechannelopener import QEChannelOpener
@@ -426,6 +426,7 @@ class ElectrumQmlApplication(QGuiApplication):
# TODO QT6 order of declaration is important now?
qmlRegisterType(QEAmount, 'org.electrum', 1, 0, 'Amount')
qmlRegisterType(QEBytes, 'org.electrum', 1, 0, 'Bytes')
qmlRegisterType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard')
qmlRegisterType(QETermsOfUseWizard, 'org.electrum', 1, 0, 'QTermsOfUseWizard')
qmlRegisterType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard')

View File

@@ -3,8 +3,8 @@ import os
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Qt
from PyQt6.QtGui import QGuiApplication
from electrum.util import send_exception_to_crash_reporter, UserFacingException
from electrum.simple_config import SimpleConfig
from electrum.gui.qml.qetypes import QEBytes
from electrum.util import send_exception_to_crash_reporter
from electrum.logging import get_logger
from electrum.i18n import _
@@ -21,14 +21,14 @@ if 'ANDROID_DATA' in os.environ:
class QEQRScanner(QObject):
_logger = get_logger(__name__)
found = pyqtSignal()
foundText = pyqtSignal(str)
foundBinary = pyqtSignal(QEBytes)
finished = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._hint = _("Scan a QR code.")
self._scan_data = "" # decoded qr code result
self.finished.connect(self._unbind, Qt.ConnectionType.QueuedConnection)
self.destroyed.connect(lambda: self.on_destroy())
@@ -44,14 +44,6 @@ class QEQRScanner(QObject):
def hint(self, v: str):
self._hint = v
@pyqtProperty(str)
def scanData(self):
return self._scan_data
@scanData.setter
def scanData(self, v: str):
self._scan_data = v
@pyqtSlot()
def open(self):
if 'ANDROID_DATA' not in os.environ:
@@ -67,9 +59,11 @@ class QEQRScanner(QObject):
def on_qr_activity_result(self, requestCode, resultCode, intent):
try:
if resultCode == -1: # RESULT_OK:
contents = intent.getStringExtra(jString("text"))
self.scanData = contents
self.found.emit()
if (contents := intent.getStringExtra(jString("text"))) is not None:
self.foundText.emit(contents)
if (contents := intent.getByteArrayExtra(jString("binary"))) is not None:
self._binary_content = QEBytes(bytes(contents.tolist()))
self.foundBinary.emit(self._binary_content)
except Exception as e: # exc would otherwise get lost
send_exception_to_crash_reporter(e)
finally:
@@ -82,23 +76,6 @@ class QEQRScanner(QObject):
def _scan_qr_non_android(self):
data = QGuiApplication.clipboard().text()
self.scanData = data
self.found.emit()
self.foundText.emit(data)
self.finished.emit()
return
# from electrum import qrscanner
# from .qeapp import ElectrumQmlApplication
# daemon = ElectrumQmlApplication._daemon
# config = daemon.config # type: SimpleConfig
# try:
# video_dev = config.get_video_device()
# data = qrscanner.scan_barcode(video_dev)
# if data is not None:
# self.scanData = data
# self.found.emit()
# except UserFacingException as e:
# self._logger.warning(f'camera error: {e!r}')
# #self.show_error(e)
# except Exception as e:
# self._logger.exception('camera error')
# #self.show_error(repr(e))

View File

@@ -116,3 +116,27 @@ class QEAmount(QObject):
def __repr__(self):
return f"<QEAmount max={self._is_max} sats={self._amount_sat} msats={self._amount_msat} empty={self.isEmpty}>"
class QEBytes(QObject):
def __init__(self, data: bytes = None, *, parent=None):
super().__init__(parent)
self.data = data
@property
def data(self):
return self._data
@data.setter
def data(self, _data):
self._data = _data
@pyqtProperty(bool)
def isEmpty(self):
return self._data is None or self._data == bytes()
def __str__(self):
return f'{self._data}'
def __repr__(self):
return f"<QEBytes data={'None' if self._data is None else self._data.hex()}>"