-
-
Notifications
You must be signed in to change notification settings - Fork 235
Expand file tree
/
Copy pathBarcodeImageWriterTask.java
More file actions
354 lines (302 loc) · 12.7 KB
/
BarcodeImageWriterTask.java
File metadata and controls
354 lines (302 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
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.NonNull;
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.nio.charset.StandardCharsets;
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;
public BarcodeImageWriterTask(
Context context, ImageView imageView, String cardIdString,
CatimaBarcode barcodeFormat, @NonNull 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<>();
// We don't want to pass the ISO-8859-1 as an encoding hint as zxing may add this 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.
// See:
// - https://github.com/CatimaLoyalty/Android/issues/2921
// - https://github.com/CatimaLoyalty/Android/issues/2932
//
// Just not always passing the encoding hint is slightly hacky, but in 5+ years of Catima
// cards without encode hints have never caused any issues (unless they were UTF-8), yet
// just days after passing ISO-8859-1 as CHARACTER_SET in the encode hints already 2
// scan failures were reported (one for QR, one for Aztec).
if (!Objects.equals(encoding.name(), StandardCharsets.ISO_8859_1.name())) {
Log.d(TAG, "Chosen encoding is not ISO_8859_1, so passing as encoding hint");
encodeHints.put(EncodeHintType.CHARACTER_SET, encoding);
} else {
Log.d(TAG, "Not passing encoding as encoding hint");
}
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();
}
}