From 6c7bfb4c50575d00ead68401c20f7e7a32b86188 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 22 Mar 2024 20:15:27 +0100 Subject: [PATCH 1/5] Initial PDF import through More Options menu --- .../protect/card_locker/ScanActivity.java | 33 +++++++--- .../main/java/protect/card_locker/Utils.java | 61 +++++++++++++++++-- app/src/main/res/values/strings.xml | 3 + 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index 01fc3854dc..f9ddfb8d4b 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -62,6 +62,7 @@ public class ScanActivity extends CatimaAppCompatActivity { private static final int COMPAT_SCALE_FACTOR_DIP = 320; private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100; + private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101; private CaptureManager capture; private DecoratedBarcodeView barcodeScannerView; @@ -73,6 +74,7 @@ public class ScanActivity extends CatimaAppCompatActivity { private ActivityResultLauncher manualAddLauncher; // can't use the pre-made contract because that launches the file manager for image type instead of gallery private ActivityResultLauncher photoPickerLauncher; + private ActivityResultLauncher pdfPickerLauncher; static final String STATE_SCANNER_ACTIVE = "scannerActive"; private boolean mScannerActive = true; @@ -99,6 +101,7 @@ protected void onCreate(Bundle savedInstanceState) { manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData())); photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData())); + pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData())); customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> { setScannerActive(false); @@ -108,7 +111,8 @@ protected void onCreate(Bundle savedInstanceState) { new CharSequence[]{ getString(R.string.addWithoutBarcode), getString(R.string.addManually), - getString(R.string.addFromImage) + getString(R.string.addFromImage), + getString(R.string.addFromPdfFile) }, (dialogInterface, i) -> { switch (i) { @@ -121,6 +125,9 @@ protected void onCreate(Bundle savedInstanceState) { case 2: addFromImage(); break; + case 3: + addFromPdfFile(); + break; default: throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option"); } @@ -364,19 +371,23 @@ public void addFromImage() { PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE); } - private void addFromImageAfterPermission() { + public void addFromPdfFile() { + PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF); + } + + private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher launcher, int chooserText, int errorMessage) { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); - photoPickerIntent.setType("image/*"); + photoPickerIntent.setType(mimeType); Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT); - contentIntent.setType("image/*"); + contentIntent.setType(mimeType); - Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(R.string.addFromImage)); + Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText)); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent }); try { - photoPickerLauncher.launch(chooserIntent); + launcher.launch(chooserIntent); } catch (ActivityNotFoundException e) { setScannerActive(true); - Toast.makeText(getApplicationContext(), R.string.failedLaunchingPhotoPicker, Toast.LENGTH_LONG).show(); + Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show(); Log.e(TAG, "No activity found to handle intent", e); } } @@ -424,9 +435,13 @@ public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] if (requestCode == CaptureManager.getCameraPermissionReqCode()) { showCameraPermissionMissingText(!granted); - } else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) { + } else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF) { if (granted) { - addFromImageAfterPermission(); + if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) { + addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker); + } else { + addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager); + } } else { setScannerActive(true); Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 1fb6c16ea1..5ac65c8276 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -12,8 +12,10 @@ import android.graphics.Color; import android.graphics.ImageDecoder; import android.graphics.Matrix; +import android.graphics.pdf.PdfRenderer; import android.net.Uri; import android.os.Build; +import android.os.ParcelFileDescriptor; import android.provider.MediaStore; import android.text.Layout; import android.text.Spanned; @@ -83,12 +85,13 @@ public class Utils { public static final int SELECT_BARCODE_REQUEST = 2; public static final int BARCODE_SCAN = 3; public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4; - public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 5; - public static final int CARD_IMAGE_FROM_CAMERA_BACK = 6; - public static final int CARD_IMAGE_FROM_CAMERA_ICON = 7; - public static final int CARD_IMAGE_FROM_FILE_FRONT = 8; - public static final int CARD_IMAGE_FROM_FILE_BACK = 9; - public static final int CARD_IMAGE_FROM_FILE_ICON = 10; + public static final int BARCODE_IMPORT_FROM_PDF_FILE = 5; + public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 6; + public static final int CARD_IMAGE_FROM_CAMERA_BACK = 7; + public static final int CARD_IMAGE_FROM_CAMERA_ICON = 8; + public static final int CARD_IMAGE_FROM_FILE_FRONT = 9; + public static final int CARD_IMAGE_FROM_FILE_BACK = 10; + public static final int CARD_IMAGE_FROM_FILE_ICON = 11; public static final String CARD_IMAGE_FILENAME_REGEX = "^(card_)(\\d+)(_(?:front|back|icon)\\.png)$"; @@ -183,6 +186,52 @@ static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int r return barcodeFromBitmap; } + if (requestCode == Utils.BARCODE_IMPORT_FROM_PDF_FILE) { + Log.i(TAG, "Received PDF file with possible barcode"); + + Uri data = intent.getData(); + if (data == null) { + Log.e(TAG, "Intent did not contain any data"); + Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + ParcelFileDescriptor parcelFileDescriptor; + PdfRenderer renderer; + try { + parcelFileDescriptor = context.getContentResolver().openFileDescriptor(data, "r"); + renderer = new PdfRenderer(parcelFileDescriptor); + } catch (IOException e) { + Log.e(TAG, "Could not read file in intent"); + Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + // Loop over all pages to find a barcode + BarcodeValues barcodeFromBitmap; + Bitmap renderedPage; + for (int i = 0; i < renderer.getPageCount(); i++) { + PdfRenderer.Page page = renderer.openPage(i); + renderedPage = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888); + page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); + page.close(); + + barcodeFromBitmap = getBarcodeFromBitmap(renderedPage); + + if (!barcodeFromBitmap.isEmpty()) { + // We found a barcode, stop scanning + renderer.close(); + return barcodeFromBitmap; + } + } + renderer.close(); + + Log.i(TAG, "No barcode found in image file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + + return new BarcodeValues(null, null); + } + if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) { if (requestCode == Utils.BARCODE_SCAN) { Log.i(TAG, "Received barcode information from camera"); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6e009e194..752b3ff307 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,4 +341,7 @@ Spend Receive Invalid amount + Select a PDF file + Could not read the file + Could not find a supported file manager From cbc888624176780d20e3e408ed454386724c9f02 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 22 Mar 2024 21:12:53 +0100 Subject: [PATCH 2/5] Load barcode from PDF when sharing to Catima --- app/src/main/AndroidManifest.xml | 1 + .../protect/card_locker/MainActivity.java | 32 +--- .../main/java/protect/card_locker/Utils.java | 152 +++++++++--------- 3 files changed, 87 insertions(+), 98 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7c3db457ef..2f3c1c4c55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + LUMINANCE_MIDPOINT; } + static public BarcodeValues retrieveBarcodeFromImage(Context context, Uri uri) { + Log.i(TAG, "Received image file with possible barcode"); + + if (uri == null) { + Log.e(TAG, "Uri did not contain any data"); + Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + Bitmap bitmap; + try { + bitmap = retrieveImageFromUri(context, uri); + } catch (IOException e) { + Log.e(TAG, "Error getting data from image file"); + e.printStackTrace(); + Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + BarcodeValues barcodeFromBitmap = getBarcodeFromBitmap(bitmap); + + if (barcodeFromBitmap.isEmpty()) { + Log.i(TAG, "No barcode found in image file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + } + + Log.i(TAG, "Read barcode id: " + barcodeFromBitmap.content()); + Log.i(TAG, "Read format: " + barcodeFromBitmap.format()); + + return barcodeFromBitmap; + } + + static public BarcodeValues retrieveBarcodeFromPdf(Context context, Uri uri) { + Log.i(TAG, "Received PDF file with possible barcode"); + + if (uri == null) { + Log.e(TAG, "Uri did not contain any data"); + Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + ParcelFileDescriptor parcelFileDescriptor; + PdfRenderer renderer; + try { + parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r"); + renderer = new PdfRenderer(parcelFileDescriptor); + } catch (IOException e) { + Log.e(TAG, "Could not read file in uri"); + Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); + return new BarcodeValues(null, null); + } + + // Loop over all pages to find a barcode + BarcodeValues barcodeFromBitmap; + Bitmap renderedPage; + for (int i = 0; i < renderer.getPageCount(); i++) { + PdfRenderer.Page page = renderer.openPage(i); + renderedPage = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888); + page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); + page.close(); + + barcodeFromBitmap = getBarcodeFromBitmap(renderedPage); + + if (!barcodeFromBitmap.isEmpty()) { + // We found a barcode, stop scanning + renderer.close(); + return barcodeFromBitmap; + } + } + renderer.close(); + + Log.i(TAG, "No barcode found in image file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + + return new BarcodeValues(null, null); + } + /** * Returns the Barcode format and content based on the result of an activity. * It shows toasts to notify the end-user as needed itself and will return an empty @@ -154,82 +231,11 @@ static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int r } if (requestCode == Utils.BARCODE_IMPORT_FROM_IMAGE_FILE) { - Log.i(TAG, "Received image file with possible barcode"); - - Uri data = intent.getData(); - if (data == null) { - Log.e(TAG, "Intent did not contain any data"); - Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); - } - - Bitmap bitmap; - try { - bitmap = retrieveImageFromUri(context, data); - } catch (IOException e) { - Log.e(TAG, "Error getting data from image file"); - e.printStackTrace(); - Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); - } - - BarcodeValues barcodeFromBitmap = getBarcodeFromBitmap(bitmap); - - if (barcodeFromBitmap.isEmpty()) { - Log.i(TAG, "No barcode found in image file"); - Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); - } - - Log.i(TAG, "Read barcode id: " + barcodeFromBitmap.content()); - Log.i(TAG, "Read format: " + barcodeFromBitmap.format()); - - return barcodeFromBitmap; + return retrieveBarcodeFromImage(context, intent.getData()); } if (requestCode == Utils.BARCODE_IMPORT_FROM_PDF_FILE) { - Log.i(TAG, "Received PDF file with possible barcode"); - - Uri data = intent.getData(); - if (data == null) { - Log.e(TAG, "Intent did not contain any data"); - Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); - } - - ParcelFileDescriptor parcelFileDescriptor; - PdfRenderer renderer; - try { - parcelFileDescriptor = context.getContentResolver().openFileDescriptor(data, "r"); - renderer = new PdfRenderer(parcelFileDescriptor); - } catch (IOException e) { - Log.e(TAG, "Could not read file in intent"); - Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); - } - - // Loop over all pages to find a barcode - BarcodeValues barcodeFromBitmap; - Bitmap renderedPage; - for (int i = 0; i < renderer.getPageCount(); i++) { - PdfRenderer.Page page = renderer.openPage(i); - renderedPage = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888); - page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); - page.close(); - - barcodeFromBitmap = getBarcodeFromBitmap(renderedPage); - - if (!barcodeFromBitmap.isEmpty()) { - // We found a barcode, stop scanning - renderer.close(); - return barcodeFromBitmap; - } - } - renderer.close(); - - Log.i(TAG, "No barcode found in image file"); - Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); - - return new BarcodeValues(null, null); + return retrieveBarcodeFromPdf(context, intent.getData()); } if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) { From 0e873b9ea5fa87538ba7b06c4e18d0b37bb3ce01 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 23 Mar 2024 11:27:03 +0100 Subject: [PATCH 3/5] Basic selector when multiple barcodes found --- ...arcodeValuesListDisambiguatorCallback.java | 6 + .../card_locker/LoyaltyCardEditActivity.java | 19 +++- .../protect/card_locker/MainActivity.java | 55 +++++---- .../protect/card_locker/ScanActivity.java | 16 ++- .../main/java/protect/card_locker/Utils.java | 105 ++++++++++++------ app/src/main/res/values/strings.xml | 1 + 6 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java diff --git a/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java b/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java new file mode 100644 index 0000000000..c5c55395d6 --- /dev/null +++ b/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java @@ -0,0 +1,6 @@ +package protect.card_locker; + +public interface BarcodeValuesListDisambiguatorCallback { + void onUserChoseBarcode(BarcodeValues barcodeValues); + void onUserDismissedSelector(); +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 7561c230c1..5d8e550327 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -646,11 +646,22 @@ public void onTabReselected(TabLayout.Tab tab) { Log.d("barcode card id editor", "barcode and card id editor picker returned without an intent"); return; } - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, getApplicationContext()); - cardId = barcodeValues.content(); - barcodeType = barcodeValues.format(); - barcodeId = ""; + List barcodeValuesList = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, getApplicationContext()); + + Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { + @Override + public void onUserChoseBarcode(BarcodeValues barcodeValues) { + cardId = barcodeValues.content(); + barcodeType = barcodeValues.format(); + barcodeId = ""; + } + + @Override + public void onUserDismissedSelector() { + + } + }); } }); diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 034e4952a3..69ab85ef64 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -7,8 +7,6 @@ import android.content.SharedPreferences; import android.database.CursorIndexOutOfBoundsException; import android.database.sqlite.SQLiteDatabase; -import android.graphics.Bitmap; -import android.net.Uri; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; @@ -33,7 +31,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; -import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -195,10 +192,12 @@ public void onDestroyActionMode(ActionMode inputMode) { @Override protected void onCreate(Bundle inputSavedInstanceState) { - extractIntentFields(getIntent()); SplashScreen.installSplashScreen(this); super.onCreate(inputSavedInstanceState); + // We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash + extractIntentFields(getIntent()); + binding = MainActivityBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); @@ -288,11 +287,11 @@ public void onClick(DialogInterface dialog, int whichButton) { } Intent intent = result.getData(); - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, this); + List barcodeValuesList = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, this); Bundle inputBundle = intent.getExtras(); String group = inputBundle != null ? inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null; - processBarcodeValues(barcodeValues, group); + processBarcodeValuesList(barcodeValuesList, group, false); }); mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { @@ -447,20 +446,32 @@ private void updateLoyaltyCardList(boolean updateCount) { } } - private void processBarcodeValues(BarcodeValues barcodeValues, String group) { - if (barcodeValues.isEmpty()) { + private void processBarcodeValuesList(List barcodeValuesList, String group, boolean closeAppOnNoBarcode) { + if (barcodeValuesList.isEmpty()) { throw new IllegalArgumentException("barcodesValues may not be empty"); } - Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); - Bundle newBundle = new Bundle(); - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format()); - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content()); - if (group != null) { - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group); - } - newIntent.putExtras(newBundle); - startActivity(newIntent); + Utils.makeUserChooseBarcodeFromList(MainActivity.this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { + @Override + public void onUserChoseBarcode(BarcodeValues barcodeValues) { + Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); + Bundle newBundle = new Bundle(); + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format()); + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content()); + if (group != null) { + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group); + } + newIntent.putExtras(newBundle); + startActivity(newIntent); + } + + @Override + public void onUserDismissedSelector() { + if (closeAppOnNoBarcode) { + finish(); + } + } + }); } private void onSharedIntent(Intent intent) { @@ -469,23 +480,23 @@ private void onSharedIntent(Intent intent) { // Check if an image or file was shared to us if (Intent.ACTION_SEND.equals(receivedAction)) { - BarcodeValues barcodeValues; + List barcodeValuesList; if (receivedType.startsWith("image/")) { - barcodeValues = Utils.retrieveBarcodeFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); } else if (receivedType.equals("application/pdf")) { - barcodeValues = Utils.retrieveBarcodeFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); } else { Log.e(TAG, "Wrong mime-type"); return; } - if (barcodeValues.isEmpty()) { + if (barcodeValuesList.isEmpty()) { finish(); return; } - processBarcodeValues(barcodeValues, null); + processBarcodeValuesList(barcodeValuesList, null, true); } } diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index f9ddfb8d4b..9679751bbc 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -275,14 +275,24 @@ private void returnResult(String barcodeContents, String barcodeFormat) { private void handleActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); + List barcodeValuesList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); - if (barcodeValues.isEmpty()) { + if (barcodeValuesList.isEmpty()) { setScannerActive(true); return; } - returnResult(barcodeValues.content(), barcodeValues.format()); + Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { + @Override + public void onUserChoseBarcode(BarcodeValues barcodeValues) { + returnResult(barcodeValues.content(), barcodeValues.format()); + } + + @Override + public void onUserDismissedSelector() { + setScannerActive(true); + } + }); } private void addWithoutBarcode() { diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index d40a9fdd60..7ebf43c3fc 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -41,6 +41,7 @@ import androidx.palette.graphics.Palette; import com.google.android.material.color.DynamicColors; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.zxing.BinaryBitmap; import com.google.zxing.LuminanceSource; import com.google.zxing.MultiFormatReader; @@ -48,6 +49,8 @@ import com.google.zxing.RGBLuminanceSource; import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.multi.GenericMultipleBarcodeReader; +import com.google.zxing.multi.MultipleBarcodeReader; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -66,6 +69,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; @@ -134,13 +138,13 @@ static public boolean needsDarkForeground(Integer backgroundColor) { return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT; } - static public BarcodeValues retrieveBarcodeFromImage(Context context, Uri uri) { + static public List retrieveBarcodesFromImage(Context context, Uri uri) { Log.i(TAG, "Received image file with possible barcode"); if (uri == null) { Log.e(TAG, "Uri did not contain any data"); Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); + return new ArrayList<>(); } Bitmap bitmap; @@ -150,29 +154,26 @@ static public BarcodeValues retrieveBarcodeFromImage(Context context, Uri uri) { Log.e(TAG, "Error getting data from image file"); e.printStackTrace(); Toast.makeText(context, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); + return new ArrayList<>(); } - BarcodeValues barcodeFromBitmap = getBarcodeFromBitmap(bitmap); + List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); - if (barcodeFromBitmap.isEmpty()) { + if (barcodesFromBitmap.isEmpty()) { Log.i(TAG, "No barcode found in image file"); Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); } - Log.i(TAG, "Read barcode id: " + barcodeFromBitmap.content()); - Log.i(TAG, "Read format: " + barcodeFromBitmap.format()); - - return barcodeFromBitmap; + return barcodesFromBitmap; } - static public BarcodeValues retrieveBarcodeFromPdf(Context context, Uri uri) { + static public List retrieveBarcodesFromPdf(Context context, Uri uri) { Log.i(TAG, "Received PDF file with possible barcode"); if (uri == null) { Log.e(TAG, "Uri did not contain any data"); Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); + return new ArrayList<>(); } ParcelFileDescriptor parcelFileDescriptor; @@ -183,11 +184,11 @@ static public BarcodeValues retrieveBarcodeFromPdf(Context context, Uri uri) { } catch (IOException e) { Log.e(TAG, "Could not read file in uri"); Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); - return new BarcodeValues(null, null); + return new ArrayList<>(); } // Loop over all pages to find a barcode - BarcodeValues barcodeFromBitmap; + List barcodesFromPdfPages = new ArrayList<>(); Bitmap renderedPage; for (int i = 0; i < renderer.getPageCount(); i++) { PdfRenderer.Page page = renderer.openPage(i); @@ -195,20 +196,16 @@ static public BarcodeValues retrieveBarcodeFromPdf(Context context, Uri uri) { page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); - barcodeFromBitmap = getBarcodeFromBitmap(renderedPage); - - if (!barcodeFromBitmap.isEmpty()) { - // We found a barcode, stop scanning - renderer.close(); - return barcodeFromBitmap; - } + barcodesFromPdfPages.addAll(getBarcodesFromBitmap(renderedPage)); } renderer.close(); - Log.i(TAG, "No barcode found in image file"); - Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + if (barcodesFromPdfPages.isEmpty()) { + Log.i(TAG, "No barcode found in pdf file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + } - return new BarcodeValues(null, null); + return barcodesFromPdfPages; } /** @@ -222,20 +219,20 @@ static public BarcodeValues retrieveBarcodeFromPdf(Context context, Uri uri) { * @param context * @return BarcodeValues */ - static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) { + static public List parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) { String contents; String format; if (resultCode != Activity.RESULT_OK) { - return new BarcodeValues(null, null); + return new ArrayList<>(); } if (requestCode == Utils.BARCODE_IMPORT_FROM_IMAGE_FILE) { - return retrieveBarcodeFromImage(context, intent.getData()); + return retrieveBarcodesFromImage(context, intent.getData()); } if (requestCode == Utils.BARCODE_IMPORT_FROM_PDF_FILE) { - return retrieveBarcodeFromPdf(context, intent.getData()); + return retrieveBarcodesFromPdf(context, intent.getData()); } if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) { @@ -251,7 +248,7 @@ static public BarcodeValues parseSetBarcodeActivityResult(int requestCode, int r Log.i(TAG, "Read barcode id: " + contents); Log.i(TAG, "Read format: " + format); - return new BarcodeValues(format, contents); + return Collections.singletonList(new BarcodeValues(format, contents)); } throw new UnsupportedOperationException("Unknown request code for parseSetBarcodeActivityResult"); @@ -271,22 +268,22 @@ private static Bitmap getBitmapSdkLessThan29(Uri data, Context context) throws I return MediaStore.Images.Media.getBitmap(context.getContentResolver(), data); } - static public BarcodeValues getBarcodeFromBitmap(Bitmap bitmap) { + static public List getBarcodesFromBitmap(Bitmap bitmap) { // This function is vulnerable to OOM, so we try again with a smaller bitmap is we get OOM for (int i = 0; i < 10; i++) { try { - return Utils.getBarcodeFromBitmapReal(bitmap); + return Utils.getBarcodesFromBitmapReal(bitmap); } catch (OutOfMemoryError e) { - Log.w(TAG, "Ran OOM in getBarcodeFromBitmap! Trying again with smaller picture! Retry " + i + " of 10."); + Log.w(TAG, "Ran OOM in getBarcodesFromBitmap! Trying again with smaller picture! Retry " + i + " of 10."); bitmap = Bitmap.createScaledBitmap(bitmap, (int) Math.round(0.75 * bitmap.getWidth()), (int) Math.round(0.75 * bitmap.getHeight()), false); } } // Give up - return new BarcodeValues(null, null); + return new ArrayList<>(); } - static private BarcodeValues getBarcodeFromBitmapReal(Bitmap bitmap) { + static private List getBarcodesFromBitmapReal(Bitmap bitmap) { // In order to decode it, the Bitmap must first be converted into a pixel array... int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()]; bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); @@ -295,13 +292,49 @@ static private BarcodeValues getBarcodeFromBitmapReal(Bitmap bitmap) { LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray); BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); + List barcodeValuesList = new ArrayList<>(); try { - Result barcodeResult = new MultiFormatReader().decode(binaryBitmap); + MultiFormatReader multiFormatReader = new MultiFormatReader(); + MultipleBarcodeReader multipleBarcodeReader = new GenericMultipleBarcodeReader(multiFormatReader); + + Result[] barcodeResults = multipleBarcodeReader.decodeMultiple(binaryBitmap); + + for (Result barcodeResult : barcodeResults) { + Log.i(TAG, "Read barcode id: " + barcodeResult.getText()); + Log.i(TAG, "Read format: " + barcodeResult.getBarcodeFormat().name()); + + barcodeValuesList.add(new BarcodeValues(barcodeResult.getBarcodeFormat().name(), barcodeResult.getText())); + } - return new BarcodeValues(barcodeResult.getBarcodeFormat().name(), barcodeResult.getText()); + return barcodeValuesList; } catch (NotFoundException e) { - return new BarcodeValues(null, null); + return barcodeValuesList; + } + } + + static public void makeUserChooseBarcodeFromList(Context context, List barcodeValuesList, BarcodeValuesListDisambiguatorCallback callback) { + // If there is only one choice, consider it chosen + if (barcodeValuesList.size() == 1) { + callback.onUserChoseBarcode(barcodeValuesList.get(0)); + return; + } + + // Ask user to choose a barcode + // TODO: This should contain an image of the barcode in question to help users understand the choice they're making + CharSequence[] barcodeDescriptions = new CharSequence[barcodeValuesList.size()]; + for (int i = 0; i < barcodeValuesList.size(); i++) { + CatimaBarcode catimaBarcode = CatimaBarcode.fromName(barcodeValuesList.get(i).format()); + barcodeDescriptions[i] = catimaBarcode.prettyName() + ": " + barcodeValuesList.get(i).content(); } + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + builder.setTitle(context.getString(R.string.multiple_barcodes_found_choose_one)); + builder.setItems( + barcodeDescriptions, + (dialogInterface, i) -> callback.onUserChoseBarcode(barcodeValuesList.get(i)) + ); + builder.setOnCancelListener(dialogInterface -> callback.onUserDismissedSelector()); + builder.show(); } static public Boolean isNotYetValid(Date validFromDate) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 752b3ff307..a67df074ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,4 +344,5 @@ Select a PDF file Could not read the file Could not find a supported file manager + Which of the found barcodes do you want to use? From 1429abd94d985552127dee251a9640e698292bdb Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 23 Mar 2024 17:54:29 +0100 Subject: [PATCH 4/5] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30486c6362..9e0943ad14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased - 134 + +- Support for scanning PDF files for barcodes +- Support for image files with multiple barcodes + ## v2.28.0 - 133 (2024-03-08) - Target Android 14 From 8519e12aa79387a9a7fd008221a01c0afcaaa9b5 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 24 Mar 2024 13:07:29 +0100 Subject: [PATCH 5/5] Add page number to barcode selector for PDF scan results --- .../protect/card_locker/BarcodeValues.java | 11 +++++--- .../main/java/protect/card_locker/Utils.java | 26 +++++++++++++++---- app/src/main/res/values/strings.xml | 3 ++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/protect/card_locker/BarcodeValues.java b/app/src/main/java/protect/card_locker/BarcodeValues.java index 681d1572fd..e9b745fd54 100644 --- a/app/src/main/java/protect/card_locker/BarcodeValues.java +++ b/app/src/main/java/protect/card_locker/BarcodeValues.java @@ -3,12 +3,17 @@ public class BarcodeValues { private final String mFormat; private final String mContent; + private String mNote; public BarcodeValues(String format, String content) { mFormat = format; mContent = content; } + public void setNote(String note) { + mNote = note; + } + public String format() { return mFormat; } @@ -17,7 +22,5 @@ public String content() { return mContent; } - public boolean isEmpty() { - return mFormat == null && mContent == null; - } -} + public String note() { return mNote; } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 7ebf43c3fc..f3df29134b 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -187,7 +187,7 @@ static public List retrieveBarcodesFromPdf(Context context, Uri u return new ArrayList<>(); } - // Loop over all pages to find a barcode + // Loop over all pages to find barcodes List barcodesFromPdfPages = new ArrayList<>(); Bitmap renderedPage; for (int i = 0; i < renderer.getPageCount(); i++) { @@ -196,7 +196,11 @@ static public List retrieveBarcodesFromPdf(Context context, Uri u page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); - barcodesFromPdfPages.addAll(getBarcodesFromBitmap(renderedPage)); + List barcodesFromPage = getBarcodesFromBitmap(renderedPage); + for (BarcodeValues barcodeValues : barcodesFromPage) { + barcodeValues.setNote(String.format(context.getString(R.string.pageWithNumber), i+1)); + barcodesFromPdfPages.add(barcodeValues); + } } renderer.close(); @@ -323,12 +327,24 @@ static public void makeUserChooseBarcodeFromList(Context context, List 22) { + barcodeContent = barcodeContent.substring(0, 20) + "…"; + } + + if (barcodeValues.note() != null) { + barcodeDescriptions[i] = String.format("%s: %s (%s)", barcodeValues.note(), catimaBarcode.prettyName(), barcodeContent); + } else { + barcodeDescriptions[i] = String.format("%s (%s)", catimaBarcode.prettyName(), barcodeContent); + } } MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(context.getString(R.string.multiple_barcodes_found_choose_one)); + builder.setTitle(context.getString(R.string.multipleBarcodesFoundPleaseChooseOne)); builder.setItems( barcodeDescriptions, (dialogInterface, i) -> callback.onUserChoseBarcode(barcodeValuesList.get(i)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a67df074ad..0c6473e256 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,5 +344,6 @@ Select a PDF file Could not read the file Could not find a supported file manager - Which of the found barcodes do you want to use? + Which of the found barcodes do you want to use? + Page %d