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 000000000..c5c55395d --- /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 7561c230c..5d8e55032 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 class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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 034e4952a..69ab85ef6 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.Intent; 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.dialog.MaterialAlertDialogBuilder; 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 class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard @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 class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } 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 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } } - 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 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard // 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 f9ddfb8d4..9679751bb 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 @@ public class ScanActivity extends CatimaAppCompatActivity { 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 d40a9fdd6..7ebf43c3f 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.exifinterface.media.ExifInterface; 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.NotFoundException; 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.NumberFormat; 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 @@ public class Utils { 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 @@ public class Utils { 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 @@ public class Utils { } 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 @@ public class Utils { 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 @@ public class Utils { * @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 @@ public class Utils { 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 @@ public class Utils { 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,15 +292,51 @@ public class Utils { 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); - return new BarcodeValues(barcodeResult.getBarcodeFormat().name(), barcodeResult.getText()); + 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 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) { // 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 752b3ff30..a67df074a 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?