Craig Raw
4 years ago
14 changed files with 448 additions and 6 deletions
Binary file not shown.
@ -1 +1 @@ |
|||
Subproject commit 8d49cebcaca6ccb2ea699fe8141554d1470d0a97 |
|||
Subproject commit 97cf49276a5c87425682a3fd0e48ffe081ff71bb |
@ -0,0 +1,191 @@ |
|||
package com.sparrowwallet.sparrow.control; |
|||
|
|||
import com.github.sarxos.webcam.WebcamResolution; |
|||
import com.sparrowwallet.drongo.Utils; |
|||
import com.sparrowwallet.drongo.protocol.Base43; |
|||
import com.sparrowwallet.drongo.protocol.Transaction; |
|||
import com.sparrowwallet.drongo.psbt.PSBT; |
|||
import com.sparrowwallet.sparrow.ur.ResultType; |
|||
import com.sparrowwallet.sparrow.ur.UR; |
|||
import com.sparrowwallet.sparrow.ur.URDecoder; |
|||
import javafx.application.Platform; |
|||
import javafx.beans.value.ChangeListener; |
|||
import javafx.beans.value.ObservableValue; |
|||
import javafx.scene.control.ButtonBar; |
|||
import javafx.scene.control.ButtonType; |
|||
import javafx.scene.control.Dialog; |
|||
import javafx.scene.control.DialogPane; |
|||
import javafx.scene.layout.StackPane; |
|||
import org.controlsfx.tools.Borders; |
|||
|
|||
public class QRScanDialog extends Dialog<QRScanDialog.Result> { |
|||
private final URDecoder decoder; |
|||
private final WebcamService webcamService; |
|||
|
|||
private boolean isUr; |
|||
private QRScanDialog.Result result; |
|||
|
|||
public QRScanDialog() { |
|||
this.decoder = new URDecoder(); |
|||
|
|||
this.webcamService = new WebcamService(WebcamResolution.VGA); |
|||
WebcamView webcamView = new WebcamView(webcamService); |
|||
|
|||
final DialogPane dialogPane = getDialogPane(); |
|||
|
|||
StackPane stackPane = new StackPane(); |
|||
stackPane.getChildren().add(webcamView.getView()); |
|||
|
|||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll()); |
|||
|
|||
webcamService.resultProperty().addListener(new QRResultListener()); |
|||
webcamService.setOnFailed(failedEvent -> { |
|||
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException()))); |
|||
}); |
|||
webcamService.start(); |
|||
setOnCloseRequest(event -> { |
|||
Platform.runLater(webcamService::cancel); |
|||
}); |
|||
|
|||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); |
|||
dialogPane.getButtonTypes().addAll(cancelButtonType); |
|||
dialogPane.setPrefWidth(660); |
|||
dialogPane.setPrefHeight(550); |
|||
|
|||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null); |
|||
} |
|||
|
|||
private class QRResultListener implements ChangeListener<com.google.zxing.Result> { |
|||
@Override |
|||
public void changed(ObservableValue<? extends com.google.zxing.Result> observable, com.google.zxing.Result oldValue, com.google.zxing.Result qrResult) { |
|||
if(result != null) { |
|||
Platform.runLater(() -> setResult(result)); |
|||
} |
|||
|
|||
//Try text first
|
|||
String qrtext = qrResult.getText(); |
|||
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) { |
|||
isUr = true; |
|||
decoder.receivePart(qrtext); |
|||
|
|||
if(decoder.getResult() != null) { |
|||
URDecoder.Result urResult = decoder.getResult(); |
|||
if(urResult.type == ResultType.SUCCESS) { |
|||
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) { |
|||
try { |
|||
PSBT psbt = new PSBT(urResult.ur.toBytes()); |
|||
result = new Result(psbt); |
|||
return; |
|||
} catch(Exception e) { |
|||
//ignore, bytes not parsable as PSBT
|
|||
} |
|||
|
|||
try { |
|||
Transaction transaction = new Transaction(urResult.ur.toBytes()); |
|||
result = new Result(transaction); |
|||
return; |
|||
} catch(Exception e) { |
|||
//ignore, bytes not parsable as tx
|
|||
} |
|||
|
|||
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction"); |
|||
} else { |
|||
result = new Result("Cannot parse UR type of " + urResult.ur.getType()); |
|||
} |
|||
} else { |
|||
result = new Result(urResult.error); |
|||
} |
|||
} |
|||
} else { |
|||
PSBT psbt; |
|||
Transaction transaction; |
|||
try { |
|||
psbt = PSBT.fromString(qrtext); |
|||
result = new Result(psbt); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as Base64 or hex
|
|||
} |
|||
|
|||
try { |
|||
psbt = new PSBT(qrResult.getRawBytes()); |
|||
result = new Result(psbt); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as raw bytes
|
|||
} |
|||
|
|||
try { |
|||
transaction = new Transaction(Utils.hexToBytes(qrtext)); |
|||
result = new Result(transaction); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as hex
|
|||
} |
|||
|
|||
try { |
|||
transaction = new Transaction(qrResult.getRawBytes()); |
|||
result = new Result(transaction); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as raw bytes
|
|||
} |
|||
|
|||
//Try Base43 used by Electrum
|
|||
byte[] base43 = Base43.decode(qrResult.getText()); |
|||
try { |
|||
psbt = new PSBT(base43); |
|||
result = new Result(psbt); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as base43 decoded bytes
|
|||
} |
|||
|
|||
try { |
|||
transaction = new Transaction(base43); |
|||
result = new Result(transaction); |
|||
return; |
|||
} catch(Exception e) { |
|||
//Ignore, not parseable as base43 decoded bytes
|
|||
} |
|||
|
|||
result = new Result("Cannot parse QR code into a PSBT or transaction"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static class Result { |
|||
public final Transaction transaction; |
|||
public final PSBT psbt; |
|||
public final String error; |
|||
public final Throwable exception; |
|||
|
|||
public Result(Transaction transaction) { |
|||
this.transaction = transaction; |
|||
this.psbt = null; |
|||
this.error = null; |
|||
this.exception = null; |
|||
} |
|||
|
|||
public Result(PSBT psbt) { |
|||
this.transaction = null; |
|||
this.psbt = psbt; |
|||
this.error = null; |
|||
this.exception = null; |
|||
} |
|||
|
|||
public Result(String error) { |
|||
this.transaction = null; |
|||
this.psbt = null; |
|||
this.error = error; |
|||
this.exception = null; |
|||
} |
|||
|
|||
public Result(Throwable exception) { |
|||
this.transaction = null; |
|||
this.psbt = null; |
|||
this.error = null; |
|||
this.exception = exception; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,81 @@ |
|||
package com.sparrowwallet.sparrow.control; |
|||
|
|||
import com.github.sarxos.webcam.Webcam; |
|||
import com.github.sarxos.webcam.WebcamResolution; |
|||
import com.google.zxing.*; |
|||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource; |
|||
import com.google.zxing.common.HybridBinarizer; |
|||
import javafx.beans.property.ObjectProperty; |
|||
import javafx.beans.property.SimpleObjectProperty; |
|||
import javafx.concurrent.Service; |
|||
import javafx.concurrent.Task; |
|||
import javafx.embed.swing.SwingFXUtils; |
|||
import javafx.scene.image.Image; |
|||
|
|||
import java.awt.image.BufferedImage; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
public class WebcamService extends Service<Image> { |
|||
private final WebcamResolution resolution ; |
|||
|
|||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null); |
|||
|
|||
public WebcamService(WebcamResolution resolution) { |
|||
this.resolution = resolution; |
|||
} |
|||
|
|||
@Override |
|||
public Task<Image> createTask() { |
|||
return new Task<Image>() { |
|||
@Override |
|||
protected Image call() throws Exception { |
|||
Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0); |
|||
try { |
|||
cam.setCustomViewSizes(resolution.getSize()); |
|||
cam.setViewSize(resolution.getSize()); |
|||
|
|||
cam.open(); |
|||
while(!isCancelled()) { |
|||
if(cam.isImageNew()) { |
|||
BufferedImage bimg = cam.getImage(); |
|||
updateValue(SwingFXUtils.toFXImage(bimg, null)); |
|||
readQR(bimg); |
|||
} |
|||
} |
|||
cam.close(); |
|||
return getValue(); |
|||
} finally { |
|||
cam.close(); |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
|
|||
private void readQR(BufferedImage bufferedImage) { |
|||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); |
|||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); |
|||
|
|||
try { |
|||
Result result = new MultiFormatReader().decode(bitmap); |
|||
resultProperty.set(result); |
|||
} catch(NotFoundException e) { |
|||
// fall thru, it means there is no QR code in image
|
|||
} |
|||
} |
|||
|
|||
public Result getResult() { |
|||
return resultProperty.get(); |
|||
} |
|||
|
|||
public ObjectProperty<Result> resultProperty() { |
|||
return resultProperty; |
|||
} |
|||
|
|||
public int getCamWidth() { |
|||
return resolution.getSize().width; |
|||
} |
|||
|
|||
public int getCamHeight() { |
|||
return resolution.getSize().height; |
|||
} |
|||
} |
@ -0,0 +1,96 @@ |
|||
package com.sparrowwallet.sparrow.control; |
|||
|
|||
import javafx.scene.Node; |
|||
import javafx.scene.control.Label; |
|||
import javafx.scene.image.ImageView; |
|||
import javafx.scene.layout.Region; |
|||
|
|||
public class WebcamView { |
|||
private final ImageView imageView; |
|||
private final WebcamService service; |
|||
private final Region view; |
|||
|
|||
private final Label statusPlaceholder ; |
|||
|
|||
public WebcamView(WebcamService service) { |
|||
this.service = service ; |
|||
this.imageView = new ImageView(); |
|||
imageView.setPreserveRatio(true); |
|||
// make the cam behave like a mirror:
|
|||
imageView.setScaleX(-1); |
|||
|
|||
this.statusPlaceholder = new Label(); |
|||
this.view = new Region() { |
|||
{ |
|||
service.stateProperty().addListener((obs, oldState, newState) -> { |
|||
switch (newState) { |
|||
case READY: |
|||
statusPlaceholder.setText("Initializing"); |
|||
getChildren().setAll(statusPlaceholder); |
|||
break ; |
|||
case SCHEDULED: |
|||
statusPlaceholder.setText("Waiting"); |
|||
getChildren().setAll(statusPlaceholder); |
|||
break ; |
|||
case RUNNING: |
|||
imageView.imageProperty().unbind(); |
|||
imageView.imageProperty().bind(service.valueProperty()); |
|||
getChildren().setAll(imageView); |
|||
break ; |
|||
case CANCELLED: |
|||
imageView.imageProperty().unbind(); |
|||
imageView.setImage(null); |
|||
statusPlaceholder.setText("Stopped"); |
|||
getChildren().setAll(statusPlaceholder); |
|||
break; |
|||
case FAILED: |
|||
imageView.imageProperty().unbind(); |
|||
statusPlaceholder.setText("Error"); |
|||
getChildren().setAll(statusPlaceholder); |
|||
service.getException().printStackTrace(); |
|||
break; |
|||
case SUCCEEDED: |
|||
// unreachable...
|
|||
imageView.imageProperty().unbind(); |
|||
statusPlaceholder.setText(""); |
|||
getChildren().clear(); |
|||
} |
|||
requestLayout(); |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
protected void layoutChildren() { |
|||
super.layoutChildren(); |
|||
double w = getWidth(); |
|||
double h = getHeight(); |
|||
if (service.isRunning()) { |
|||
imageView.setFitWidth(w); |
|||
imageView.setFitHeight(h); |
|||
imageView.resizeRelocate(0, 0, w, h); |
|||
} else { |
|||
double labelHeight = statusPlaceholder.prefHeight(w); |
|||
double labelWidth = statusPlaceholder.prefWidth(labelHeight); |
|||
statusPlaceholder.resizeRelocate((w - labelWidth)/2, (h-labelHeight)/2, labelWidth, labelHeight); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected double computePrefWidth(double height) { |
|||
return service.getCamWidth(); |
|||
} |
|||
@Override |
|||
protected double computePrefHeight(double width) { |
|||
return service.getCamHeight(); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
public WebcamService getService() { |
|||
return service; |
|||
} |
|||
|
|||
public Node getView() { |
|||
return view; |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
public class RequestQRScanEvent { |
|||
//Empty event class used to request the QRScanDialog is opened
|
|||
} |
@ -1,5 +1,5 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
public class RequestTransactionOpenEvent { |
|||
//Empty event class used to request the transaction open dialog
|
|||
//Empty event class used to request the transaction open file dialog
|
|||
} |
|||
|
Loading…
Reference in new issue