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 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 @@ + 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 4f4b16b27f..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,63 +446,57 @@ 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) { String receivedAction = intent.getAction(); String receivedType = intent.getType(); - // Check if an image was shared to us + // Check if an image or file was shared to us if (Intent.ACTION_SEND.equals(receivedAction)) { - if (!receivedType.startsWith("image/")) { - Log.e(TAG, "Wrong mime-type"); - return; - } - - BarcodeValues barcodeValues; - Bitmap bitmap; + List barcodeValuesList; - Uri data = intent.getParcelableExtra(Intent.EXTRA_STREAM); - if (data == null) { - Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - finish(); - return; - } - - try { - bitmap = Utils.retrieveImageFromUri(this, data); - } catch (IOException e) { - Log.e(TAG, "Error getting data from image file"); - e.printStackTrace(); - Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - finish(); + if (receivedType.startsWith("image/")) { + barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + } else if (receivedType.equals("application/pdf")) { + barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + } else { + Log.e(TAG, "Wrong mime-type"); return; } - barcodeValues = Utils.getBarcodeFromBitmap(bitmap); - - if (barcodeValues.isEmpty()) { - Log.i(TAG, "No barcode found in image file"); - Toast.makeText(this, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + 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 01fc3854dc..9679751bbc 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"); } @@ -268,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() { @@ -364,19 +381,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 +445,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..f3df29134b 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; @@ -39,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; @@ -46,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; @@ -64,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; @@ -83,12 +89,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)$"; @@ -131,6 +138,80 @@ static public boolean needsDarkForeground(Integer backgroundColor) { return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT; } + 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 ArrayList<>(); + } + + 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 ArrayList<>(); + } + + List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); + + if (barcodesFromBitmap.isEmpty()) { + Log.i(TAG, "No barcode found in image file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + } + + return barcodesFromBitmap; + } + + 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 ArrayList<>(); + } + + 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 ArrayList<>(); + } + + // Loop over all pages to find barcodes + List barcodesFromPdfPages = new ArrayList<>(); + 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(); + + List barcodesFromPage = getBarcodesFromBitmap(renderedPage); + for (BarcodeValues barcodeValues : barcodesFromPage) { + barcodeValues.setNote(String.format(context.getString(R.string.pageWithNumber), i+1)); + barcodesFromPdfPages.add(barcodeValues); + } + } + renderer.close(); + + if (barcodesFromPdfPages.isEmpty()) { + Log.i(TAG, "No barcode found in pdf file"); + Toast.makeText(context, R.string.noBarcodeFound, Toast.LENGTH_LONG).show(); + } + + return barcodesFromPdfPages; + } + /** * 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 @@ -142,45 +223,20 @@ static public boolean needsDarkForeground(Integer backgroundColor) { * @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) { - 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 retrieveBarcodesFromImage(context, intent.getData()); + } - return barcodeFromBitmap; + if (requestCode == Utils.BARCODE_IMPORT_FROM_PDF_FILE) { + return retrieveBarcodesFromPdf(context, intent.getData()); } if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) { @@ -196,7 +252,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"); @@ -216,22 +272,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()); @@ -240,15 +296,63 @@ 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++) { + BarcodeValues barcodeValues = barcodeValuesList.get(i); + CatimaBarcode catimaBarcode = CatimaBarcode.fromName(barcodeValues.format()); + + String barcodeContent = barcodeValues.content(); + // Shorten overly long barcodes + if (barcodeContent.length() > 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.multipleBarcodesFoundPleaseChooseOne)); + builder.setItems( + barcodeDescriptions, + (dialogInterface, i) -> callback.onUserChoseBarcode(barcodeValuesList.get(i)) + ); + builder.setOnCancelListener(dialogInterface -> callback.onUserDismissedSelector()); + builder.show(); + } + static public Boolean isNotYetValid(Date validFromDate) { // The note in `hasExpired` does not apply here, since the bug was fixed before this feature was added. return validFromDate.after(getStartOfToday().getTime()); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6e009e194..0c6473e256 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,4 +341,9 @@ Spend Receive Invalid amount + 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? + Page %d