Skip to content

Commit

Permalink
Merge pull request #7050 from devinbileck/scan-qr-code
Browse files Browse the repository at this point in the history
Restore QR code scanner feature for mobile notification pairing
  • Loading branch information
alejandrogarcia83 authored Apr 30, 2024
2 parents 4fc0ab5 + 5f3f692 commit add2f98
Show file tree
Hide file tree
Showing 20 changed files with 1,584 additions and 8 deletions.
1 change: 1 addition & 0 deletions core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,7 @@ account.notifications.priceAlert.message.title=Price alert for {0}
account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2}
account.notifications.noWebCamFound.warning=No webcam found.\n\n\
Please use the email option to send the token and encryption key from your mobile phone to the Bisq application.
account.notifications.webcam.error=An error has occurred with the webcam.
account.notifications.priceAlert.warning.highPriceTooLow=The higher price must be larger than the lower price.
account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must be lower than the higher price.

Expand Down
1 change: 1 addition & 0 deletions desktop/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation libs.fontawesomefx.commons
implementation libs.fontawesomefx.materialdesign.font
implementation libs.qrgen
implementation libs.javacv
implementation libs.apache.commons.lang3
implementation libs.bouncycastle.bcpg.jdk15on
implementation libs.fxmisc.easybind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.InfoInputTextField;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.account.content.notifications.qr.FrameToBitmapConverter;
import bisq.desktop.main.account.content.notifications.qr.FrameToImageConverter;
import bisq.desktop.main.account.content.notifications.qr.ImageCaptureDeviceFinder;
import bisq.desktop.main.account.content.notifications.qr.ImageCaptureDeviceNotFoundException;
import bisq.desktop.main.account.content.notifications.qr.ImageCaptureReader;
import bisq.desktop.main.account.content.notifications.qr.QrCodeProcessor;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.WebCamWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
Expand Down Expand Up @@ -89,12 +96,15 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
private final MarketAlerts marketAlerts;
private final MobileNotificationService mobileNotificationService;

private WebCamWindow webCamWindow;
private ImageCaptureReader<String> qrCodeReader;

private TextField tokenInputTextField;
private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField;
private ToggleButton useSoundToggleButton, tradeToggleButton, marketToggleButton, priceToggleButton;
private ComboBox<TradeCurrency> currencyComboBox;
private ComboBox<PaymentAccount> paymentAccountsComboBox;
private Button downloadButton, eraseButton, setPriceAlertButton,
private Button downloadButton, webCamButton, noWebCamButton, eraseButton, setPriceAlertButton,
removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/;

private ChangeListener<Boolean> useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener,
Expand Down Expand Up @@ -145,6 +155,8 @@ protected void activate() {
// setup
tokenInputTextField.textProperty().addListener(tokenInputTextFieldListener);
downloadButton.setOnAction(e -> onDownload());
webCamButton.setOnAction(e -> onOpenWebCam());
noWebCamButton.setOnAction(e -> onNoWebCam());
// testMsgButton.setOnAction(e -> onSendTestMsg());
eraseButton.setOnAction(e -> onErase());

Expand Down Expand Up @@ -197,6 +209,8 @@ protected void deactivate() {
// setup
tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener);
downloadButton.setOnAction(null);
webCamButton.setOnAction(null);
noWebCamButton.setOnAction(null);
//testMsgButton.setOnAction(null);
eraseButton.setOnAction(null);

Expand Down Expand Up @@ -235,6 +249,63 @@ private void onDownload() {
GUIUtil.openWebPage("https://bisq.network/downloads");
}

private void onOpenWebCam() {
webCamButton.setDisable(true);
new ImageCaptureDeviceFinder(imageCaptureDevice -> {
imageCaptureDevice.setImageWidth(640);
imageCaptureDevice.setImageHeight(480);
webCamWindow = new WebCamWindow(
imageCaptureDevice.getImageWidth(),
imageCaptureDevice.getImageHeight()
).onClose(() -> {
webCamButton.setDisable(false);
qrCodeReader.close();
});
webCamWindow.show();

qrCodeReader = new ImageCaptureReader<>(
imageCaptureDevice,
new FrameToImageConverter(),
new QrCodeProcessor(new FrameToBitmapConverter()),
webCamWindow.getImageView(),
qrCode -> {
webCamWindow.hide();
webCamButton.setDisable(false);
reset();
tokenInputTextField.setText(qrCode);
updateMarketAlertFields();
updatePriceAlertFields();
}, exception -> {
if (exception instanceof ImageCaptureDeviceNotFoundException) {
new Popup().warning(Res.get("account.notifications.noWebCamFound.warning")).show();
webCamWindow.hide();
webCamButton.setDisable(false);
onNoWebCam();
} else {
log.error("{0}", exception);
new Popup().error(Res.get("account.notifications.webcam.error")).show();
webCamWindow.hide();
webCamButton.setDisable(false);
}
});
}, exception -> {
if (exception instanceof ImageCaptureDeviceNotFoundException) {
new Popup().warning(Res.get("account.notifications.noWebCamFound.warning")).show();
webCamButton.setDisable(false);
onNoWebCam();
} else {
log.error("{0}", exception);
new Popup().error(Res.get("account.notifications.webcam.error")).show();
}
});
}

private void onNoWebCam() {
setPairingTokenFieldsVisible();
noWebCamButton.setManaged(false);
noWebCamButton.setVisible(false);
}

private void onErase() {
try {
mobileNotificationService.sendEraseMessage();
Expand Down Expand Up @@ -354,10 +425,18 @@ private void createSetupFields() {
Res.get("account.notifications.download.label"),
Layout.TWICE_FIRST_ROW_DISTANCE);

Tuple3<Label, Button, Button> tuple = addTopLabel2Buttons(root, ++gridRow,
Res.get("account.notifications.webcam.label"),
Res.get("account.notifications.webcam.button"), Res.get("account.notifications.noWebcam.button"), 0);
webCamButton = tuple.second;
noWebCamButton = tuple.third;

tokenInputTextField = addInputTextField(root, ++gridRow,
Res.get("account.notifications.email.label"));
tokenInputTextField.setPromptText(Res.get("account.notifications.email.prompt"));
tokenInputTextFieldListener = (observable, oldValue, newValue) -> applyKeyAndToken(newValue);
tokenInputTextField.setManaged(false);
tokenInputTextField.setVisible(false);

/*testMsgButton = FormBuilder.addTopLabelButton(root, ++gridRow, Res.get("account.notifications.testMsg.label"),
Res.get("account.notifications.testMsg.title")).second;
Expand Down Expand Up @@ -718,4 +797,3 @@ private void fillPaymentAccounts() {
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.desktop.main.account.content.notifications.qr;

import org.bytedeco.javacv.Frame;

/**
* Converts a JavaCV {@link Frame} to another object of type {@link T}.
* @param <T> The object type to convert to.
*/
public interface FrameConverter<T> {
T convert(Frame frame);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.desktop.main.account.content.notifications.qr;

import java.util.Optional;



import org.bytedeco.javacv.Frame;

/**
* Processes a JavaCV {@link Frame} and returns an {@link Optional<T>} containing the
* result if successful, or {@link Optional#empty()} if there is no result.
* @param <T> The object type of the result.
*/
public interface FrameProcessor<T> {
Optional<T> process(Frame frame);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.desktop.main.account.content.notifications.qr;

import java.util.Objects;

import lombok.Getter;

import org.jetbrains.annotations.NotNull;



import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;

/**
* Used for converting a JavaCV {@link Frame} into a ZXing {@link BinaryBitmap}.
* It is specifically designed for integration with the ZXing library, enabling
* QR code detection and decoding from OpenCV images.
*/
public class FrameToBitmapConverter implements FrameConverter<BinaryBitmap> {
private final FrameConverter<Mat> frameToMatConverter = new FrameToMatConverter();

@Override
public BinaryBitmap convert(@NotNull final Frame frame) {
Objects.requireNonNull(frame, "Frame must not be null");

final Mat mat = frameToMatConverter.convert(frame);
final OpenCVMatToGrayscaleSource source = new OpenCVMatToGrayscaleSource(mat);
return new BinaryBitmap(new HybridBinarizer(source));
}

/**
* A luminance source that facilitates the conversion of an OpenCV {@link Mat} object,
* typically in BGR format, into a grayscale image suitable for QR scanning.
*
* <p>This approach allows us to eliminate our dependency on
* {@code java.awt.BufferedImage} which is the typical intermediary.
*/
private static class OpenCVMatToGrayscaleSource extends LuminanceSource {
private final byte[] luminances;

OpenCVMatToGrayscaleSource(@NotNull final Mat mat) {
super(mat.cols(), mat.rows());
Objects.requireNonNull(mat, "Mat must not be null");
try (AutoCloseableMat autoCloseableMat = new AutoCloseableMat()) {
Mat grayMat = autoCloseableMat.getMat();

if (mat.channels() == 3) {
// Convert BGR to Grayscale
opencv_imgproc.cvtColor(mat, grayMat, opencv_imgproc.COLOR_BGR2GRAY);
} else if (mat.channels() == 1) {
grayMat = mat;
} else {
throw new IllegalArgumentException(
"Unsupported Mat format with " + mat.channels() + " channels");
}

this.luminances = new byte[grayMat.cols() * grayMat.rows()];
grayMat.data().get(this.luminances);
}
}

@Override
public byte[] getRow(int y, byte[] row) {
if (row == null || row.length < getWidth()) {
row = new byte[getWidth()];
}
System.arraycopy(luminances, y * getWidth(), row, 0, getWidth());
return row;
}

@Override
public byte[] getMatrix() {
return luminances;
}
}

/**
* A wrapper class for OpenCV's {@link Mat} that implements {@link AutoCloseable},
* facilitating the use of try-with-resources for automatic resource management.
* This class ensures that the native memory allocated by the {@link Mat} object is
* properly released when the {@link AutoCloseableMat} instance goes out of scope or
* is otherwise no longer needed.
*/
@Getter
private static class AutoCloseableMat implements AutoCloseable {
private final Mat mat;

public AutoCloseableMat() {
this.mat = new Mat();
}

@Override
public void close() {
mat.release();
}
}
}
Loading

0 comments on commit add2f98

Please sign in to comment.