diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzer.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzer.java index e1bbee30..c35861fa 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzer.java +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzer.java @@ -2,8 +2,6 @@ package com.beemdevelopment.aegis.helpers; import static android.graphics.ImageFormat.YUV_420_888; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.util.Size; @@ -70,7 +68,7 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer { Result result = reader.decode(bitmap, hints); if (_listener != null) { - new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result)); + _listener.onQrCodeDetected(result); } } catch (NotFoundException ignored) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java index 0b5b4637..39c268ba 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java @@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; @@ -170,23 +171,25 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis @Override public void onQrCodeDetected(Result result) { - if (_analysis != null) { - try { - Uri uri = Uri.parse(result.getText().trim()); - if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) { - handleExportUri(uri); - } else { - handleUri(uri); - } - } catch (GoogleAuthInfoException e) { - e.printStackTrace(); + new Handler(getMainLooper()).post(() -> { + if (_analysis != null) { + try { + Uri uri = Uri.parse(result.getText().trim()); + if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) { + handleExportUri(uri); + } else { + handleUri(uri); + } + } catch (GoogleAuthInfoException e) { + e.printStackTrace(); - unbindPreview(_cameraProvider); - Dialogs.showErrorDialog(this, - e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error, - e, ((dialog, which) -> bindPreview(_cameraProvider))); + unbindPreview(_cameraProvider); + Dialogs.showErrorDialog(this, + e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error, + e, ((dialog, which) -> bindPreview(_cameraProvider))); + } } - } + }); } private void handleUri(Uri uri) throws GoogleAuthInfoException { diff --git a/app/src/test/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzerTest.java b/app/src/test/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzerTest.java new file mode 100644 index 00000000..3aeb3faf --- /dev/null +++ b/app/src/test/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzerTest.java @@ -0,0 +1,154 @@ +package com.beemdevelopment.aegis.helpers; + +import static android.graphics.ImageFormat.YUV_420_888; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import android.media.Image; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.core.ImageInfo; +import androidx.camera.core.ImageProxy; + +import com.beemdevelopment.aegis.util.IOUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.GZIPInputStream; + +@RunWith(RobolectricTestRunner.class) +public class QrCodeAnalyzerTest { + private static final String _expectedUri = "otpauth://totp/neo4j:Charlotte?secret=B33WS2ALPT34K4BNY24AYROE4M&issuer=neo4j&algorithm=SHA1&digits=6&period=30"; + + @Test + public void testScanQrCode() { + boolean found = scan("qr.y.gz", 1600, 1200, 1600); + assertTrue("QR code not found", found); + } + + @Test + public void testScanStridedQrCode() { + boolean found = scan("qr.strided.y.gz", 1840, 1380, 1840); + assertFalse("QR code found", found); + + found = scan("qr.strided.y.gz", 1840, 1380, 1856); + assertTrue("QR code not found", found); + } + + private boolean scan(String fileName, int width, int height, int rowStride) { + AtomicBoolean found = new AtomicBoolean(); + QrCodeAnalyzer analyzer = new QrCodeAnalyzer(result -> { + assertEquals(_expectedUri, result.getText()); + found.set(true); + }); + + FakeImageProxy imgProxy; + try (InputStream inStream = getClass().getResourceAsStream(fileName); + GZIPInputStream zipStream = new GZIPInputStream(inStream)) { + imgProxy = new FakeImageProxy(IOUtils.readAll(zipStream), width, height, rowStride); + } catch (IOException e) { + throw new RuntimeException(e); + } + + analyzer.analyze(imgProxy); + return found.get(); + } + + private static class FakePlaneProxy implements ImageProxy.PlaneProxy { + private final byte[] _y; + private final int _rowStride; + + public FakePlaneProxy(byte[] y, int rowStride) { + _y = y; + _rowStride = rowStride; + } + + @Override + public int getRowStride() { + return _rowStride; + } + + @Override + public int getPixelStride() { + return 1; + } + + @NonNull + @Override + public ByteBuffer getBuffer() { + return ByteBuffer.wrap(_y); + } + } + + private static class FakeImageProxy implements ImageProxy { + private final byte[] _y; + private final int _width; + private final int _height; + private final int _rowStride; + + public FakeImageProxy(byte[] y, int width, int height, int rowStride) { + _y = y; + _width = width; + _height = height; + _rowStride = rowStride; + } + + @Override + public void close() { + + } + + @NonNull + @Override + public Rect getCropRect() { + return null; + } + + @Override + public void setCropRect(@Nullable @org.jetbrains.annotations.Nullable Rect rect) { + + } + + @Override + public int getFormat() { + return YUV_420_888; + } + + @Override + public int getHeight() { + return _height; + } + + @Override + public int getWidth() { + return _width; + } + + @NonNull + @Override + public ImageProxy.PlaneProxy[] getPlanes() { + return new PlaneProxy[]{new FakePlaneProxy(_y, _rowStride)}; + } + + @NonNull + @Override + public ImageInfo getImageInfo() { + return null; + } + + @Nullable + @Override + public Image getImage() { + return null; + } + } +} diff --git a/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.strided.y.gz b/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.strided.y.gz new file mode 100644 index 00000000..9235957c Binary files /dev/null and b/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.strided.y.gz differ diff --git a/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.y.gz b/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.y.gz new file mode 100644 index 00000000..565e9318 Binary files /dev/null and b/app/src/test/resources/com/beemdevelopment/aegis/helpers/qr.y.gz differ