mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 14:02:49 +00:00
Try harder to find QR codes in image files
And refactor a bit by moving some of the QR scanning related logic to a separate helper class.
This commit is contained in:
parent
5f12eae678
commit
bd3697659f
6 changed files with 184 additions and 82 deletions
|
@ -9,19 +9,11 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.camera.core.ImageAnalysis;
|
import androidx.camera.core.ImageAnalysis;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.BinaryBitmap;
|
|
||||||
import com.google.zxing.DecodeHintType;
|
|
||||||
import com.google.zxing.MultiFormatReader;
|
|
||||||
import com.google.zxing.NotFoundException;
|
import com.google.zxing.NotFoundException;
|
||||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||||
import com.google.zxing.Result;
|
import com.google.zxing.Result;
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||||
|
@ -59,14 +51,8 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
MultiFormatReader reader = new MultiFormatReader();
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
|
||||||
try {
|
try {
|
||||||
Map<DecodeHintType, Object> hints = new HashMap<>();
|
Result result = QrCodeHelper.decodeFromSource(source);
|
||||||
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
|
|
||||||
hints.put(DecodeHintType.ALSO_INVERTED, true);
|
|
||||||
|
|
||||||
Result result = reader.decode(bitmap, hints);
|
|
||||||
if (_listener != null) {
|
if (_listener != null) {
|
||||||
_listener.onQrCodeDetected(result);
|
_listener.onQrCodeDetected(result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.beemdevelopment.aegis.helpers;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.BinaryBitmap;
|
||||||
|
import com.google.zxing.DecodeHintType;
|
||||||
|
import com.google.zxing.LuminanceSource;
|
||||||
|
import com.google.zxing.MultiFormatReader;
|
||||||
|
import com.google.zxing.NotFoundException;
|
||||||
|
import com.google.zxing.RGBLuminanceSource;
|
||||||
|
import com.google.zxing.Result;
|
||||||
|
import com.google.zxing.WriterException;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class QrCodeHelper {
|
||||||
|
private QrCodeHelper() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
|
||||||
|
Map<DecodeHintType, Object> hints = new HashMap<>();
|
||||||
|
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
|
||||||
|
hints.put(DecodeHintType.ALSO_INVERTED, true);
|
||||||
|
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
|
MultiFormatReader reader = new MultiFormatReader();
|
||||||
|
return reader.decode(bitmap, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
|
||||||
|
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
||||||
|
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
|
||||||
|
if (bitmap == null) {
|
||||||
|
throw new DecodeError("Unable to decode stream to bitmap");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ZXing is not able to decode the image on the first try, we try a couple of
|
||||||
|
// more times with smaller versions of the same image.
|
||||||
|
for (int i = 0; i <= 2; i++) {
|
||||||
|
if (i != 0) {
|
||||||
|
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||||
|
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||||
|
|
||||||
|
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
|
||||||
|
return decodeFromSource(source);
|
||||||
|
} catch (NotFoundException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DecodeError(NotFoundException.getNotFoundInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
|
||||||
|
QRCodeWriter writer = new QRCodeWriter();
|
||||||
|
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
|
||||||
|
|
||||||
|
int[] pixels = new int[width * height];
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
int offset = y * width;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DecodeError extends Exception {
|
||||||
|
public DecodeError(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecodeError(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
@ -26,32 +24,19 @@ import com.beemdevelopment.aegis.Preferences;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.SortCategory;
|
import com.beemdevelopment.aegis.SortCategory;
|
||||||
import com.beemdevelopment.aegis.ViewMode;
|
import com.beemdevelopment.aegis.ViewMode;
|
||||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
|
||||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||||
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
|
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
|
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
|
||||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||||
|
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
|
||||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.zxing.BinaryBitmap;
|
|
||||||
import com.google.zxing.ChecksumException;
|
|
||||||
import com.google.zxing.FormatException;
|
|
||||||
import com.google.zxing.LuminanceSource;
|
|
||||||
import com.google.zxing.NotFoundException;
|
|
||||||
import com.google.zxing.RGBLuminanceSource;
|
|
||||||
import com.google.zxing.Reader;
|
|
||||||
import com.google.zxing.Result;
|
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
|
||||||
import com.google.zxing.qrcode.QRCodeReader;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -326,37 +311,25 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onScanImageResult(Intent intent) {
|
private void onScanImageResult(Intent intent) {
|
||||||
decodeQrCodeImage(intent.getData());
|
startDecodeQrCodeImage(intent.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decodeQrCodeImage(Uri inputFile) {
|
private void startDecodeQrCodeImage(Uri uri) {
|
||||||
Bitmap bitmap;
|
QrDecodeTask task = new QrDecodeTask(this, (result) -> {
|
||||||
|
if (result.getException() != null) {
|
||||||
try {
|
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, result.getException());
|
||||||
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
|
return;
|
||||||
|
|
||||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
|
||||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
|
||||||
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
try {
|
||||||
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
|
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getResult().getText());
|
||||||
|
VaultEntry entry = new VaultEntry(info);
|
||||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
} catch (GoogleAuthInfoException e) {
|
||||||
|
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||||
Reader reader = new QRCodeReader();
|
}
|
||||||
Result result = reader.decode(binaryBitmap);
|
});
|
||||||
|
task.execute(getLifecycle(), uri);
|
||||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
|
||||||
VaultEntry entry = new VaultEntry(info);
|
|
||||||
|
|
||||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
|
||||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSortCategoryMenu() {
|
private void updateSortCategoryMenu() {
|
||||||
|
@ -471,7 +444,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
intent.setAction(null);
|
intent.setAction(null);
|
||||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||||
|
|
||||||
decodeQrCodeImage(uri);
|
startDecodeQrCodeImage(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,10 @@ import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.Theme;
|
import com.beemdevelopment.aegis.Theme;
|
||||||
|
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.google.zxing.BarcodeFormat;
|
|
||||||
import com.google.zxing.WriterException;
|
import com.google.zxing.WriterException;
|
||||||
import com.google.zxing.common.BitMatrix;
|
|
||||||
import com.google.zxing.qrcode.QRCodeWriter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -104,22 +102,12 @@ public class TransferEntriesActivity extends AegisActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void generateQR() {
|
private void generateQR() {
|
||||||
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||||
_issuer.setText(selectedEntry.getIssuer());
|
_issuer.setText(selectedEntry.getIssuer());
|
||||||
_accountName.setText(selectedEntry.getAccountName());
|
_accountName.setText(selectedEntry.getAccountName());
|
||||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||||
|
|
||||||
QRCodeWriter writer = new QRCodeWriter();
|
|
||||||
BitMatrix bitMatrix;
|
|
||||||
try {
|
|
||||||
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
|
|
||||||
} catch (WriterException e) {
|
|
||||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt int backgroundColor = Color.WHITE;
|
@ColorInt int backgroundColor = Color.WHITE;
|
||||||
if (getConfiguredTheme() == Theme.LIGHT) {
|
if (getConfiguredTheme() == Theme.LIGHT) {
|
||||||
TypedValue typedValue = new TypedValue();
|
TypedValue typedValue = new TypedValue();
|
||||||
|
@ -127,18 +115,14 @@ public class TransferEntriesActivity extends AegisActivity {
|
||||||
backgroundColor = typedValue.data;
|
backgroundColor = typedValue.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
int width = bitMatrix.getWidth();
|
Bitmap bitmap;
|
||||||
int height = bitMatrix.getHeight();
|
try {
|
||||||
int[] pixels = new int[width * height];
|
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
|
||||||
for (int y = 0; y < height; y++) {
|
} catch (WriterException e) {
|
||||||
int offset = y * width;
|
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||||
for (int x = 0; x < width; x++) {
|
return;
|
||||||
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
||||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
|
||||||
_qrImage.setImageBitmap(bitmap);
|
_qrImage.setImageBitmap(bitmap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.beemdevelopment.aegis.ui.tasks;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.R;
|
||||||
|
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
|
||||||
|
import com.google.zxing.Result;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class QrDecodeTask extends ProgressDialogTask<Uri, QrDecodeTask.Response> {
|
||||||
|
private final Callback _cb;
|
||||||
|
|
||||||
|
public QrDecodeTask(Context context, Callback cb) {
|
||||||
|
super(context, context.getString(R.string.analyzing_qr));
|
||||||
|
_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Response doInBackground(Uri... params) {
|
||||||
|
Context context = getDialog().getContext();
|
||||||
|
|
||||||
|
Uri uri = params[0];
|
||||||
|
try (InputStream inStream = context.getContentResolver().openInputStream(uri)) {
|
||||||
|
Result result = QrCodeHelper.decodeFromStream(inStream);
|
||||||
|
return new Response(result, null);
|
||||||
|
} catch (QrCodeHelper.DecodeError | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return new Response(null, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Response result) {
|
||||||
|
super.onPostExecute(result);
|
||||||
|
_cb.onTaskFinished(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onTaskFinished(Response result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response {
|
||||||
|
private final Result _result;
|
||||||
|
private final Exception _e;
|
||||||
|
|
||||||
|
public Response(Result result, Exception e) {
|
||||||
|
_result = result;
|
||||||
|
_e = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result getResult() {
|
||||||
|
return _result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception getException() {
|
||||||
|
return _e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,6 +156,7 @@
|
||||||
<string name="encrypting_vault">Encrypting the vault</string>
|
<string name="encrypting_vault">Encrypting the vault</string>
|
||||||
<string name="exporting_vault">Exporting the vault</string>
|
<string name="exporting_vault">Exporting the vault</string>
|
||||||
<string name="reading_file">Reading file</string>
|
<string name="reading_file">Reading file</string>
|
||||||
|
<string name="analyzing_qr">Analyzing QR code</string>
|
||||||
<string name="importing_icon_pack">Importing icon pack</string>
|
<string name="importing_icon_pack">Importing icon pack</string>
|
||||||
<string name="delete_entry">Delete entry</string>
|
<string name="delete_entry">Delete entry</string>
|
||||||
<string name="delete_entry_description">Are you sure you want to delete this entry?</string>
|
<string name="delete_entry_description">Are you sure you want to delete this entry?</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue