mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2026-01-16 10:58:01 -05:00
If zxing is not explicitly told a barcode is UTF-8, it may render it incorrectly. Which caused https://github.com/CatimaLoyalty/Android/issues/2555. However, when an encode hint is set, it will cause zxing to set an ECI hint inside the barcode, which some scanners may trip over and cause scanning failures, leading to https://github.com/CatimaLoyalty/Android/issues/2921. This change only passes the encoding in automatic mode if zxing explicitly guesses it to be UTF-8, and otherwise doesn't pass anything, to keep the ECI empty. This might need to be expanded for other types like SJIS, but as nobody ever reported such a bug let's assume it's not necessary for now.
352 lines
12 KiB
Java
352 lines
12 KiB
Java
package protect.card_locker;
|
|
|
|
import android.content.Context;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Color;
|
|
import android.graphics.PorterDuff;
|
|
import android.util.ArrayMap;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.View;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.google.zxing.EncodeHintType;
|
|
import com.google.zxing.MultiFormatWriter;
|
|
import com.google.zxing.WriterException;
|
|
import com.google.zxing.common.BitMatrix;
|
|
import com.google.zxing.common.StringUtils;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
import java.nio.charset.Charset;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
|
|
import protect.card_locker.async.CompatCallable;
|
|
|
|
/**
|
|
* This task will generate a barcode and load it into an ImageView.
|
|
* Only a weak reference of the ImageView is kept, so this class will not
|
|
* prevent the ImageView from being garbage collected.
|
|
*/
|
|
public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
|
|
private static final String TAG = "Catima";
|
|
|
|
private static final int IS_VALID = 999;
|
|
private final Context mContext;
|
|
private boolean isSuccesful;
|
|
|
|
// When drawn in a smaller window 1D barcodes for some reason end up
|
|
// squished, whereas 2D barcodes look fine.
|
|
private static final int MAX_WIDTH_1D = 1500;
|
|
private static final int MAX_WIDTH_2D = 500;
|
|
|
|
private final WeakReference<ImageView> imageViewReference;
|
|
private final WeakReference<TextView> textViewReference;
|
|
private String cardId;
|
|
private final CatimaBarcode format;
|
|
private final Charset encoding;
|
|
private final int imageHeight;
|
|
private final int imageWidth;
|
|
private final int imagePadding;
|
|
private final boolean widthPadding;
|
|
private final boolean showFallback;
|
|
private final BarcodeImageWriterResultCallback callback;
|
|
|
|
BarcodeImageWriterTask(
|
|
Context context, ImageView imageView, String cardIdString,
|
|
CatimaBarcode barcodeFormat, @Nullable Charset barcodeEncoding, TextView textView,
|
|
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding, boolean isFullscreen
|
|
) {
|
|
mContext = context;
|
|
|
|
isSuccesful = true;
|
|
this.callback = callback;
|
|
|
|
// Use a WeakReference to ensure the ImageView can be garbage collected
|
|
imageViewReference = new WeakReference<>(imageView);
|
|
textViewReference = new WeakReference<>(textView);
|
|
|
|
cardId = cardIdString;
|
|
format = barcodeFormat;
|
|
encoding = barcodeEncoding;
|
|
|
|
int imageViewHeight = imageView.getHeight();
|
|
int imageViewWidth = imageView.getWidth();
|
|
|
|
// Some barcodes already have internal whitespace and shouldn't get extra padding
|
|
// TODO: Get rid of this hack by somehow detecting this extra whitespace
|
|
if (roundCornerPadding && !barcodeFormat.hasInternalPadding()) {
|
|
imagePadding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, context.getResources().getDisplayMetrics()));
|
|
} else {
|
|
imagePadding = 0;
|
|
}
|
|
|
|
if (format.isSquare() && imageViewWidth > imageViewHeight) {
|
|
imageViewWidth -= imagePadding;
|
|
widthPadding = true;
|
|
} else {
|
|
imageViewHeight -= imagePadding;
|
|
widthPadding = false;
|
|
}
|
|
|
|
final int MAX_WIDTH = getMaxWidth(format);
|
|
|
|
if (format.isSquare()) {
|
|
imageHeight = imageWidth = Math.min(imageViewHeight, Math.min(MAX_WIDTH, imageViewWidth));
|
|
} else if (imageView.getWidth() < MAX_WIDTH && !isFullscreen) {
|
|
imageHeight = imageViewHeight;
|
|
imageWidth = imageViewWidth;
|
|
} else {
|
|
// Scale down the image to reduce the memory needed to produce it
|
|
imageWidth = Math.min(MAX_WIDTH, this.mContext.getResources().getDisplayMetrics().widthPixels);
|
|
double ratio = (double) imageWidth / (double) imageViewWidth;
|
|
imageHeight = (int) (imageViewHeight * ratio);
|
|
}
|
|
|
|
this.showFallback = showFallback;
|
|
}
|
|
|
|
private int getMaxWidth(CatimaBarcode format) {
|
|
switch (format.format()) {
|
|
// 2D barcodes
|
|
case AZTEC:
|
|
case MAXICODE:
|
|
case PDF_417:
|
|
case QR_CODE:
|
|
return MAX_WIDTH_2D;
|
|
|
|
// 2D but rectangular versions get blurry otherwise
|
|
case DATA_MATRIX:
|
|
return MAX_WIDTH_1D;
|
|
|
|
// 1D barcodes:
|
|
case CODABAR:
|
|
case CODE_39:
|
|
case CODE_93:
|
|
case CODE_128:
|
|
case EAN_8:
|
|
case EAN_13:
|
|
case ITF:
|
|
case UPC_A:
|
|
case UPC_E:
|
|
case RSS_14:
|
|
case RSS_EXPANDED:
|
|
case UPC_EAN_EXTENSION:
|
|
default:
|
|
return MAX_WIDTH_1D;
|
|
}
|
|
}
|
|
|
|
private String getFallbackString(CatimaBarcode format) {
|
|
switch (format.format()) {
|
|
// 2D barcodes
|
|
case AZTEC:
|
|
return "AZTEC";
|
|
case DATA_MATRIX:
|
|
return "DATA_MATRIX";
|
|
case PDF_417:
|
|
return "PDF_417";
|
|
case QR_CODE:
|
|
return "QR_CODE";
|
|
|
|
// 1D barcodes:
|
|
case CODABAR:
|
|
return "C0C";
|
|
case CODE_39:
|
|
return "CODE_39";
|
|
case CODE_93:
|
|
return "CODE_93";
|
|
case CODE_128:
|
|
return "CODE_128";
|
|
case EAN_8:
|
|
return "32123456";
|
|
case EAN_13:
|
|
return "5901234123457";
|
|
case ITF:
|
|
return "1003";
|
|
case UPC_A:
|
|
return "123456789012";
|
|
case UPC_E:
|
|
return "0123456";
|
|
default:
|
|
throw new IllegalArgumentException("No fallback known for this barcode type");
|
|
}
|
|
}
|
|
|
|
private Bitmap generate() {
|
|
if (cardId.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
MultiFormatWriter writer = new MultiFormatWriter();
|
|
|
|
Map<EncodeHintType, Object> encodeHints = new ArrayMap<>();
|
|
// Use charset if defined or guess otherwise
|
|
if (encoding != null) {
|
|
Log.d(TAG, "Encoding explicitly set, " + encoding.name());
|
|
encodeHints.put(EncodeHintType.CHARACTER_SET, encoding);
|
|
} else {
|
|
String guessedEncoding = StringUtils.guessEncoding(cardId.getBytes(), new ArrayMap<>());
|
|
Log.d(TAG, "Guessed encoding: " + guessedEncoding);
|
|
|
|
// We don't want to pass the gussed encoding as an encoding hint unless it is UTF-8 as
|
|
// zxing is likely to add the mentioned encoding hint as ECI inside the barcode.
|
|
//
|
|
// Due to many barcode scanners in the wild being badly coded they may trip over ECI
|
|
// info existing and fail to scan, such as in https://github.com/CatimaLoyalty/Android/issues/2921
|
|
if (Objects.equals(guessedEncoding, "UTF8")) {
|
|
Log.d(TAG, "Guessed encoding is UTF8, so passing as encoding hint");
|
|
encodeHints.put(EncodeHintType.CHARACTER_SET, Charset.forName(guessedEncoding));
|
|
}
|
|
}
|
|
|
|
BitMatrix bitMatrix;
|
|
try {
|
|
try {
|
|
bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, encodeHints);
|
|
} catch (Exception e) {
|
|
// Cast a wider net here and catch any exception, as there are some
|
|
// cases where an encoder may fail if the data is invalid for the
|
|
// barcode type. If this happens, we want to fail gracefully.
|
|
throw new WriterException(e);
|
|
}
|
|
|
|
final int WHITE = 0xFFFFFFFF;
|
|
final int BLACK = 0xFF000000;
|
|
|
|
int bitMatrixWidth = bitMatrix.getWidth();
|
|
int bitMatrixHeight = bitMatrix.getHeight();
|
|
|
|
int[] pixels = new int[bitMatrixWidth * bitMatrixHeight];
|
|
|
|
for (int y = 0; y < bitMatrixHeight; y++) {
|
|
int offset = y * bitMatrixWidth;
|
|
for (int x = 0; x < bitMatrixWidth; x++) {
|
|
int color = bitMatrix.get(x, y) ? BLACK : WHITE;
|
|
pixels[offset + x] = color;
|
|
}
|
|
}
|
|
Bitmap bitmap = Bitmap.createBitmap(bitMatrixWidth, bitMatrixHeight,
|
|
Bitmap.Config.ARGB_8888);
|
|
bitmap.setPixels(pixels, 0, bitMatrixWidth, 0, 0, bitMatrixWidth, bitMatrixHeight);
|
|
|
|
// Determine if the image needs to be scaled.
|
|
// This is necessary because the datamatrix barcode generator
|
|
// ignores the requested size and returns the smallest image necessary
|
|
// to represent the barcode. If we let the ImageView scale the image
|
|
// it will use bi-linear filtering, which results in a blurry barcode.
|
|
// To avoid this, if scaling is needed do so without filtering.
|
|
|
|
int heightScale = imageHeight / bitMatrixHeight;
|
|
int widthScale = imageWidth / bitMatrixHeight;
|
|
int scalingFactor = Math.min(heightScale, widthScale);
|
|
|
|
if (scalingFactor > 1) {
|
|
bitmap = Bitmap.createScaledBitmap(bitmap, bitMatrixWidth * scalingFactor, bitMatrixHeight * scalingFactor, false);
|
|
}
|
|
|
|
return bitmap;
|
|
} catch (WriterException e) {
|
|
Log.e(TAG, "Failed to generate barcode of type " + format + ": " + cardId, e);
|
|
} catch (OutOfMemoryError e) {
|
|
Log.w(TAG, "Insufficient memory to render barcode, "
|
|
+ imageWidth + "x" + imageHeight + ", " + format.name()
|
|
+ ", length=" + cardId.length(), e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Bitmap doInBackground(Void... params) {
|
|
// Only do the hard tasks if we've not already been cancelled
|
|
if (!Thread.currentThread().isInterrupted()) {
|
|
Bitmap bitmap = generate();
|
|
|
|
if (bitmap == null) {
|
|
isSuccesful = false;
|
|
|
|
if (showFallback && !Thread.currentThread().isInterrupted()) {
|
|
Log.i(TAG, "Barcode generation failed, generating fallback...");
|
|
cardId = getFallbackString(format);
|
|
bitmap = generate();
|
|
return bitmap;
|
|
}
|
|
} else {
|
|
return bitmap;
|
|
}
|
|
}
|
|
|
|
// We've been interrupted - create a empty fallback
|
|
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
|
return Bitmap.createBitmap(imageWidth, imageHeight, config);
|
|
}
|
|
|
|
public void onPostExecute(Object castResult) {
|
|
Bitmap result = (Bitmap) castResult;
|
|
|
|
Log.i(TAG, "Finished generating barcode image of type " + format + ": " + cardId);
|
|
ImageView imageView = imageViewReference.get();
|
|
if (imageView == null) {
|
|
// The ImageView no longer exists, nothing to do
|
|
return;
|
|
}
|
|
|
|
String formatPrettyName = format.prettyName();
|
|
|
|
imageView.setTag(isSuccesful);
|
|
|
|
imageView.setImageBitmap(result);
|
|
imageView.setContentDescription(mContext.getString(R.string.barcodeImageDescriptionWithType, formatPrettyName));
|
|
TextView textView = textViewReference.get();
|
|
|
|
if (result != null) {
|
|
Log.i(TAG, "Displaying barcode");
|
|
if (widthPadding) {
|
|
imageView.setPadding(imagePadding / 2, 0, imagePadding / 2, 0);
|
|
} else {
|
|
imageView.setPadding(0, imagePadding / 2, 0, imagePadding / 2);
|
|
}
|
|
imageView.setVisibility(View.VISIBLE);
|
|
|
|
if (isSuccesful) {
|
|
imageView.setColorFilter(null);
|
|
} else {
|
|
imageView.setColorFilter(Color.LTGRAY, PorterDuff.Mode.LIGHTEN);
|
|
}
|
|
|
|
if (textView != null) {
|
|
textView.setVisibility(View.VISIBLE);
|
|
textView.setText(formatPrettyName);
|
|
}
|
|
} else {
|
|
Log.i(TAG, "Barcode generation failed, removing image from display");
|
|
imageView.setVisibility(View.GONE);
|
|
if (textView != null) {
|
|
textView.setVisibility(View.GONE);
|
|
}
|
|
}
|
|
|
|
if (callback != null) {
|
|
callback.onBarcodeImageWriterResult(isSuccesful);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onPreExecute() {
|
|
// No Action
|
|
}
|
|
|
|
/**
|
|
* Provided to comply with Callable while keeping the original Syntax of AsyncTask
|
|
*
|
|
* @return generated Bitmap
|
|
*/
|
|
@Override
|
|
public Bitmap call() {
|
|
return doInBackground();
|
|
}
|
|
}
|