Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions electrum/gui/qml/components/WalletMainView.qml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ Item {
}
//scanner.destroy() // TODO
})
scanner.onFoundUR.connect(function(data) {
console.log("UR type:", data.getType)
console.log("UR data:", data.getBytes)
piResolver.recipient = data.getBytes
})
scanner.open()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.electrum.qr;

public interface ScanCallback {
boolean onPart(String text, byte[] binary);
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public class SimpleScannerActivity extends Activity {

private boolean mAlreadyRequestedPermissions = false;

private static ScanCallback callback;

public static void setCallback(ScanCallback cb) {
callback = cb;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down Expand Up @@ -64,7 +70,8 @@ public void onClick(View v) {
Toast.makeText(SimpleScannerActivity.this, "Clipboard contents too large.", Toast.LENGTH_SHORT).show();
return;
}
SimpleScannerActivity.this.setResultAndClose(null, clipboardText);
Log.v(TAG, "clipboard contentType TEXT");
SimpleScannerActivity.this.setResultAndClose(clipboardText, null);
} else {
Toast.makeText(SimpleScannerActivity.this, "Clipboard is empty.", Toast.LENGTH_SHORT).show();
}
Expand Down Expand Up @@ -104,32 +111,38 @@ private void startCamera() {
contentFrame.addView(mScannerView);
mScannerView.setOnBarcodeListener(result -> {
// Handle the scan result
this.setResultAndClose(result, null);
// Return false to stop scanning after first result
return false;
String text = null;
byte[] binary = null;
if (result.getContentType() == ContentType.TEXT) {
Log.v(TAG, "scanResult contentType TEXT");
text = result.getText();
} else if (result.getContentType() == ContentType.BINARY) {
Log.v(TAG, "scanResult contentType BINARY");
binary = result.getRawBytes();
} else {
Log.v(TAG, "scanResult contentType unknown");
return true;
}
return !this.setResultAndClose(text, binary);
});
}
mScannerView.openAsync(); // Start camera on resume
}

private void setResultAndClose(Result scanResult, String textOnly) {
Intent resultIntent = new Intent();
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");
private boolean setResultAndClose(String text, byte[] binary) {
boolean done = false;
if (callback != null) {
try {
done = callback.onPart(text, binary);
} catch (Exception e) {
Log.e(TAG, "callback failed", e);
}
}
setResult(Activity.RESULT_OK, resultIntent);
this.finish();
if (done) {
setResult(Activity.RESULT_OK, new Intent());
this.finish();
}
return done;
}

private boolean hasPermission() {
Expand Down
3 changes: 2 additions & 1 deletion electrum/gui/qml/qeapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from .qeinvoice import QEInvoice, QEInvoiceParser
from .qepiresolver import QEPIResolver
from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount, QEBytes
from .qetypes import QEAmount, QEBytes, QEUR
from .qeaddressdetails import QEAddressDetails
from .qetxdetails import QETxDetails
from .qechannelopener import QEChannelOpener
Expand Down Expand Up @@ -479,6 +479,7 @@ def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: '
# TODO QT6 order of declaration is important now?
qmlRegisterType(QEAmount, 'org.electrum', 1, 0, 'Amount')
qmlRegisterType(QEBytes, 'org.electrum', 1, 0, 'Bytes')
qmlRegisterType(QEUR, 'org.electrum', 1, 0, 'UR')
qmlRegisterType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard')
qmlRegisterType(QETermsOfUseWizard, 'org.electrum', 1, 0, 'QTermsOfUseWizard')
qmlRegisterType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard')
Expand Down
47 changes: 41 additions & 6 deletions electrum/gui/qml/qeqrscanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,43 @@
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Qt
from PyQt6.QtGui import QGuiApplication

from electrum.gui.qml.qetypes import QEBytes
from electrum.gui.qml.qetypes import QEBytes, QEUR
from electrum.util import send_exception_to_crash_reporter
from electrum.logging import get_logger
from electrum.i18n import _
from electrum.ur.ur_decoder import URDecoder


if 'ANDROID_DATA' in os.environ:
from jnius import autoclass
from jnius import autoclass, PythonJavaClass, java_method
from android import activity

jpythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity
jString = autoclass('java.lang.String')
jIntent = autoclass('android.content.Intent')


class QRPartCallback(PythonJavaClass):
__javainterfaces__ = ['org/electrum/qr/ScanCallback']
__javacontext__ = 'app'

def __init__(self, handler):
super().__init__()
self.handler = handler

@java_method('(Ljava/lang/String;[B)Z')
def onPart(self, text, binary):
return self.handler(text, binary)


class QEQRScanner(QObject):
REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY = 30368 # random 16 bit int

_logger = get_logger(__name__)

foundText = pyqtSignal(str)
foundBinary = pyqtSignal(QEBytes)
foundUR = pyqtSignal(QEUR)

finished = pyqtSignal()

Expand Down Expand Up @@ -52,6 +67,12 @@ def open(self):
self._scan_qr_non_android()
return
jSimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")

self._result = None
self._decoder = URDecoder()
self._callback = QRPartCallback(self._on_part)
jSimpleScannerActivity.setCallback(self._callback)

intent = jIntent(jpythonActivity, jSimpleScannerActivity)
intent.putExtra(jIntent.EXTRA_TEXT, jString(self._hint))

Expand All @@ -63,16 +84,30 @@ def close(self):
# no-op to prevent qml type error
pass

def _on_part(self, text, binary):
if text:
self._result = str(text)
if text.lower().startswith('ur:'):
self._decoder.receive_part(text)
if not self._decoder.is_complete():
return False
elif binary:
self._result = bytes(binary)
return True

def on_qr_activity_result(self, requestCode, resultCode, intent):
if requestCode != self.REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY:
self._logger.warning(f"got activity result with invalid {requestCode=}")
return
try:
if resultCode == -1: # RESULT_OK:
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()))
if self._decoder.is_complete():
self._ur_content = QEUR(self._decoder.result_message())
self.foundUR.emit(self._ur_content)
elif isinstance(self._result, str):
self.foundText.emit(self._result)
elif isinstance(self._result, bytes):
self._binary_content = QEBytes(self._result)
self.foundBinary.emit(self._binary_content)
except Exception as e: # exc would otherwise get lost
send_exception_to_crash_reporter(e)
Expand Down
39 changes: 39 additions & 0 deletions electrum/gui/qml/qetypes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from base64 import b64encode

from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject

from electrum.logging import get_logger
from electrum.i18n import _
from electrum.ur.cbor_lite import CBORDecoder
from electrum.ur.ur import UR


class QEAmount(QObject):
Expand Down Expand Up @@ -141,3 +145,38 @@ def __str__(self):

def __repr__(self):
return f"<QEBytes data={'None' if self._data is None else self._data.hex()}>"


class QEUR(QObject):
def __init__(self, data: UR = 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(str)
def getType(self):
if self._data is None:
return ""
return self._data.type

@pyqtProperty(str)
def getBytes(self):
if self._data is None:
return ""
data, _ = CBORDecoder(self._data.cbor).decodeBytes()
return b64encode(data).decode()

def __str__(self):
return f'{self.getBytes}'

def __repr__(self):
if self._data is None:
return "<QEUR None>"
return f"<QEUR type={self.getType} data={self.getBytes}>"
8 changes: 6 additions & 2 deletions electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the qtmultimedia toolchain is only used on Windows and macOS. Desktop Linux uses zbar.
But perhaps this is something we should reconsider.
See comment here:

# We have two toolchains to scan qr codes:
# 1. access camera via QtMultimedia, take picture, feed picture to zbar
# 2. let zbar handle whole flow (including accessing the camera)
#
# notes:
# - zbar needs to be compiled with platform-dependent extra config options to be able
# to access the camera
# - zbar fails to access the camera on macOS
# - qtmultimedia seems to support more cameras on Windows than zbar
# - qtmultimedia is often not packaged with PyQt
# in particular, on debian, you need both "python3-pyqt6" and "python3-pyqt6.qtmultimedia"
# - older versions of qtmultimedia don't seem to work reliably
#
# Considering the above, we use QtMultimedia for Windows and macOS, as there
# most users run our binaries where we can make sure the packaged versions work well.
# On Linux where many people run from source, we use zbar.

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from electrum.i18n import _
from electrum.qrreader import get_qr_reader, QrCodeResult, MissingQrDetectionLib
from electrum.logging import Logger
from electrum.ur.ur_decoder import URDecoder

from electrum.gui.qt.util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect

Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(self, parent: Optional[QWidget], *, config: SimpleConfig):
self._ok_done: bool = False
self.camera_sc_conn = None
self.resolution: QSize = None
self.ur_decoder = URDecoder()

self.config = config

Expand Down Expand Up @@ -245,6 +247,7 @@ def _on_finished(self, code):
and self.validator_res and self.validator_res.accepted
and self.validator_res.simple_result)
or '' )
res = res and (self.ur_decoder.result or res)

self.validator = None

Expand Down Expand Up @@ -291,8 +294,9 @@ def _on_frame_available(self, frame: QImage):

# Close the dialog if the validator accepted the result
if self.validator_res.accepted:
self.accept()
return
if not self.ur_decoder.receive_part(self.validator_res.simple_result):
self.accept()
return

# Apply the crop blur effect
if self.image_effect:
Expand Down
Empty file added electrum/ur/__init__.py
Empty file.
Loading