From 5faa28a7e799ba56c531363291041836e3b3deb9 Mon Sep 17 00:00:00 2001 From: waffshappen <44290023+waffshappen@users.noreply.github.com> Date: Wed, 20 Oct 2021 17:47:06 +0000 Subject: [PATCH] Replace AsyncTask with a Compatiblity Layer and further Compat Fixes (#511) --- .../card_locker/BarcodeImageWriterTask.java | 131 ++++++----- .../card_locker/BarcodeSelectorActivity.java | 111 ++++----- .../card_locker/ImportExportActivity.java | 174 +++++--------- .../protect/card_locker/ImportExportTask.java | 72 +++--- .../card_locker/LoyaltyCardCursorAdapter.java | 87 +++---- .../card_locker/LoyaltyCardEditActivity.java | 108 ++++----- .../card_locker/LoyaltyCardViewActivity.java | 168 +++++--------- .../card_locker/async/CompatCallable.java | 8 + .../card_locker/async/TaskHandler.java | 174 ++++++++++++++ .../importexport/CatimaImporter.java | 8 +- .../importexport/FidmeImporter.java | 14 +- .../importexport/StocardImporter.java | 2 +- .../BarcodeSelectorActivityTest.java | 29 +-- .../protect/card_locker/ImportExportTest.java | 219 ++++++++---------- 14 files changed, 654 insertions(+), 651 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/async/CompatCallable.java create mode 100644 app/src/main/java/protect/card_locker/async/TaskHandler.java diff --git a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java index 9a4fae608..241ebf90d 100644 --- a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java +++ b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java @@ -4,30 +4,29 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; -import android.os.AsyncTask; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.google.zxing.BarcodeFormat; import com.google.zxing.MultiFormatWriter; import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import java.lang.ref.WeakReference; +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. */ -class BarcodeImageWriterTask extends AsyncTask -{ +public class BarcodeImageWriterTask implements CompatCallable { private static final String TAG = "Catima"; private static final int IS_VALID = 999; - private Context mContext; + private final Context mContext; private boolean isSuccesful; // When drawn in a smaller window 1D barcodes for some reason end up @@ -44,10 +43,11 @@ class BarcodeImageWriterTask extends AsyncTask private final boolean showFallback; private final Runnable callback; - BarcodeImageWriterTask(Context context, ImageView imageView, String cardIdString, - CatimaBarcode barcodeFormat, TextView textView, - boolean showFallback, Runnable callback) - { + BarcodeImageWriterTask( + Context context, ImageView imageView, String cardIdString, + CatimaBarcode barcodeFormat, TextView textView, + boolean showFallback, Runnable callback + ) { mContext = context; isSuccesful = true; @@ -62,26 +62,21 @@ class BarcodeImageWriterTask extends AsyncTask final int MAX_WIDTH = getMaxWidth(format); - if(imageView.getWidth() < MAX_WIDTH) - { + if (imageView.getWidth() < MAX_WIDTH) { imageHeight = imageView.getHeight(); imageWidth = imageView.getWidth(); - } - else - { + } else { // Scale down the image to reduce the memory needed to produce it imageWidth = MAX_WIDTH; - double ratio = (double)MAX_WIDTH / (double)imageView.getWidth(); - imageHeight = (int)(imageView.getHeight() * ratio); + double ratio = (double) MAX_WIDTH / (double) imageView.getWidth(); + imageHeight = (int) (imageView.getHeight() * ratio); } this.showFallback = showFallback; } - private int getMaxWidth(CatimaBarcode format) - { - switch(format.format()) - { + private int getMaxWidth(CatimaBarcode format) { + switch (format.format()) { // 2D barcodes case AZTEC: case DATA_MATRIX: @@ -108,10 +103,8 @@ class BarcodeImageWriterTask extends AsyncTask } } - private String getFallbackString(CatimaBarcode format) - { - switch(format.format()) - { + private String getFallbackString(CatimaBarcode format) { + switch (format.format()) { // 2D barcodes case AZTEC: return "AZTEC"; @@ -144,23 +137,17 @@ class BarcodeImageWriterTask extends AsyncTask } } - private Bitmap generate() - { - if (cardId.isEmpty()) - { + private Bitmap generate() { + if (cardId.isEmpty()) { return null; } MultiFormatWriter writer = new MultiFormatWriter(); BitMatrix bitMatrix; - try - { - try - { + try { + try { bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, null); - } - catch(Exception e) - { + } 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. @@ -175,11 +162,9 @@ class BarcodeImageWriterTask extends AsyncTask int[] pixels = new int[bitMatrixWidth * bitMatrixHeight]; - for (int y = 0; y < bitMatrixHeight; y++) - { + for (int y = 0; y < bitMatrixHeight; y++) { int offset = y * bitMatrixWidth; - for (int x = 0; x < bitMatrixWidth; x++) - { + for (int x = 0; x < bitMatrixWidth; x++) { int color = bitMatrix.get(x, y) ? BLACK : WHITE; pixels[offset + x] = color; } @@ -199,19 +184,14 @@ class BarcodeImageWriterTask extends AsyncTask int widthScale = imageWidth / bitMatrixHeight; int scalingFactor = Math.min(heightScale, widthScale); - if(scalingFactor > 1) - { + if (scalingFactor > 1) { bitmap = Bitmap.createScaledBitmap(bitmap, bitMatrixWidth * scalingFactor, bitMatrixHeight * scalingFactor, false); } return bitmap; - } - catch (WriterException e) - { + } catch (WriterException e) { Log.e(TAG, "Failed to generate barcode of type " + format + ": " + cardId, e); - } - catch(OutOfMemoryError e) - { + } catch (OutOfMemoryError e) { Log.w(TAG, "Insufficient memory to render barcode, " + imageWidth + "x" + imageHeight + ", " + format.name() + ", length=" + cardId.length(), e); @@ -220,29 +200,36 @@ class BarcodeImageWriterTask extends AsyncTask return null; } - public Bitmap doInBackground(Void... params) - { - Bitmap bitmap = generate(); + 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 (bitmap == null) { + isSuccesful = false; - if (showFallback) { - Log.i(TAG, "Barcode generation failed, generating fallback..."); - cardId = getFallbackString(format); - bitmap = generate(); + if (showFallback && !Thread.currentThread().isInterrupted()) { + Log.i(TAG, "Barcode generation failed, generating fallback..."); + cardId = getFallbackString(format); + bitmap = generate(); + return bitmap; + } + } else { + return bitmap; } } - return bitmap; + // We've been interrupted - create a empty fallback + Bitmap.Config config = Bitmap.Config.ARGB_8888; + return Bitmap.createBitmap(imageWidth, imageHeight, config); } - protected void onPostExecute(Bitmap result) - { + 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) - { + if (imageView == null) { // The ImageView no longer exists, nothing to do return; } @@ -255,8 +242,7 @@ class BarcodeImageWriterTask extends AsyncTask imageView.setContentDescription(mContext.getString(R.string.barcodeImageDescriptionWithType, formatPrettyName)); TextView textView = textViewReference.get(); - if(result != null) - { + if (result != null) { Log.i(TAG, "Displaying barcode"); imageView.setVisibility(View.VISIBLE); @@ -270,9 +256,7 @@ class BarcodeImageWriterTask extends AsyncTask textView.setVisibility(View.VISIBLE); textView.setText(formatPrettyName); } - } - else - { + } else { Log.i(TAG, "Barcode generation failed, removing image from display"); imageView.setVisibility(View.GONE); if (textView != null) { @@ -284,4 +268,19 @@ class BarcodeImageWriterTask extends AsyncTask callback.run(); } } + + @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(); + } } diff --git a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java index 40ea80a93..0251e9230 100644 --- a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java +++ b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java @@ -2,8 +2,9 @@ package protect.card_locker; import android.app.Activity; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.util.Pair; import android.view.MenuItem; @@ -16,24 +17,21 @@ import android.widget.Toast; import com.google.zxing.BarcodeFormat; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.Map; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; +import protect.card_locker.async.TaskHandler; + /** * This activity is callable and will allow a user to enter * barcode data and generate all barcodes possible for * the data. The user may then select any barcode, where its * data and type will be returned to the caller. */ -public class BarcodeSelectorActivity extends CatimaAppCompatActivity -{ +public class BarcodeSelectorActivity extends CatimaAppCompatActivity { private static final String TAG = "Catima"; // Result this activity will return @@ -41,19 +39,21 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity public static final String BARCODE_FORMAT = "format"; private Map> barcodeViewMap; - private LinkedList barcodeGeneratorTasks = new LinkedList<>(); + + final private TaskHandler mTasks = new TaskHandler(); + + private final Handler typingDelayHandler = new Handler(Looper.getMainLooper()); + public static final Integer INPUT_DELAY = 250; @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.selectBarcodeTitle); setContentView(R.layout.barcode_selector_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } @@ -72,26 +72,31 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity barcodeViewMap.put(BarcodeFormat.UPC_E.name(), new Pair<>(R.id.upceBarcode, R.id.upceBarcodeText)); EditText cardId = findViewById(R.id.cardId); - cardId.addTextChangedListener(new SimpleTextWatcher() - { + + cardId.addTextChangedListener(new SimpleTextWatcher() { @Override - public void onTextChanged(CharSequence s, int start, int before, int count) - { - Log.d(TAG, "Entered text: " + s); + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Delay the input processing so we avoid overload + typingDelayHandler.removeCallbacksAndMessages(null); - generateBarcodes(s.toString()); + typingDelayHandler.postDelayed(() -> { + Log.d(TAG, "Entered text: " + s); - View noBarcodeButtonView = findViewById(R.id.noBarcode); - setButtonListener(noBarcodeButtonView, s.toString()); - noBarcodeButtonView.setEnabled(s.length() > 0); + runOnUiThread(() -> { + generateBarcodes(s.toString()); + + View noBarcodeButtonView = findViewById(R.id.noBarcode); + setButtonListener(noBarcodeButtonView, s.toString()); + noBarcodeButtonView.setEnabled(s.length() > 0); + }); + }, INPUT_DELAY); } }); final Bundle b = getIntent().getExtras(); final String initialCardId = b != null ? b.getString("initialCardId") : null; - if(initialCardId != null) - { + if (initialCardId != null) { cardId.setText(initialCardId); } else { generateBarcodes(""); @@ -99,24 +104,19 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity } private void generateBarcodes(String value) { - // Stop any async tasks which may not have been started yet - for(AsyncTask task : barcodeGeneratorTasks) - { - task.cancel(false); - } - barcodeGeneratorTasks.clear(); + // Attempt to stop any async tasks which may not have been started yet + // TODO this can be very much optimized by only generating Barcodes visible to the User + mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); // Update barcodes - for(Map.Entry> entry : barcodeViewMap.entrySet()) - { + for (Map.Entry> entry : barcodeViewMap.entrySet()) { ImageView image = findViewById(entry.getValue().first); TextView text = findViewById(entry.getValue().second); createBarcodeOption(image, entry.getKey(), value, text); } } - private void setButtonListener(final View button, final String cardId) - { + private void setButtonListener(final View button, final String cardId) { button.setOnClickListener(view -> { Log.d(TAG, "Selected no barcode"); Intent result = new Intent(); @@ -127,8 +127,7 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity }); } - private void createBarcodeOption(final ImageView image, final String formatType, final String cardId, final TextView text) - { + private void createBarcodeOption(final ImageView image, final String formatType, final String cardId, final TextView text) { final CatimaBarcode format = CatimaBarcode.fromName(formatType); image.setImageBitmap(null); @@ -147,40 +146,32 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity finish(); }); - if(image.getHeight() == 0) - { + if (image.getHeight() == 0) { // The size of the ImageView is not yet available as it has not // yet been drawn. Wait for it to be drawn so the size is available. image.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() - { - @Override - public void onGlobalLayout() - { - Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth()); - image.getViewTreeObserver().removeOnGlobalLayoutListener(this); + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth()); + image.getViewTreeObserver().removeOnGlobalLayoutListener(this); - Log.d(TAG, "Generating barcode for type " + formatType); - BarcodeImageWriterTask task = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); - barcodeGeneratorTasks.add(task); - task.execute(); - } - }); - } - else - { + Log.d(TAG, "Generating barcode for type " + formatType); + + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + } + }); + } else { Log.d(TAG, "Generating barcode for type " + formatType); - BarcodeImageWriterTask task = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); - barcodeGeneratorTasks.add(task); - task.execute(); + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } } @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if (item.getItemId() == android.R.id.home) - { + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { setResult(Activity.RESULT_CANCELED); finish(); return true; diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 40f565010..444cc2d9e 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -6,7 +6,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.text.InputType; import android.util.Log; @@ -32,11 +31,12 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; + +import protect.card_locker.async.TaskHandler; import protect.card_locker.importexport.DataFormat; import protect.card_locker.importexport.ImportExportResult; -public class ImportExportActivity extends CatimaAppCompatActivity -{ +public class ImportExportActivity extends CatimaAppCompatActivity { private static final String TAG = "Catima"; private static final int PERMISSIONS_EXTERNAL_STORAGE = 1; @@ -50,17 +50,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity private DataFormat importDataFormat; private String exportPassword; + final private TaskHandler mTasks = new TaskHandler(); + @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.importExport); setContentView(R.layout.import_export_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } @@ -69,12 +69,11 @@ public class ImportExportActivity extends CatimaAppCompatActivity if (ContextCompat.checkSelfPermission(ImportExportActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(ImportExportActivity.this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) - { + ContextCompat.checkSelfPermission(ImportExportActivity.this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(ImportExportActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE}, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_EXTERNAL_STORAGE); } @@ -85,16 +84,14 @@ public class ImportExportActivity extends CatimaAppCompatActivity intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip"); Button exportButton = findViewById(R.id.exportButton); - exportButton.setOnClickListener(new View.OnClickListener() - { + exportButton.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) - { + public void onClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(ImportExportActivity.this); builder.setTitle(R.string.exportPassword); FrameLayout container = new FrameLayout(ImportExportActivity.this); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.leftMargin = 50; params.rightMargin = 50; @@ -121,11 +118,9 @@ public class ImportExportActivity extends CatimaAppCompatActivity intentGetContentAction.setType("*/*"); Button importFilesystem = findViewById(R.id.importOptionFilesystemButton); - importFilesystem.setOnClickListener(new View.OnClickListener() - { + importFilesystem.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) - { + public void onClick(View v) { chooseImportType(intentGetContentAction); } }); @@ -134,11 +129,9 @@ public class ImportExportActivity extends CatimaAppCompatActivity final Intent intentPickAction = new Intent(Intent.ACTION_PICK); Button importApplication = findViewById(R.id.importOptionApplicationButton); - importApplication.setOnClickListener(new View.OnClickListener() - { + importApplication.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) - { + public void onClick(View v) { chooseImportType(intentPickAction); } }); @@ -200,24 +193,22 @@ public class ImportExportActivity extends CatimaAppCompatActivity .setTitle(importAlertTitle) .setMessage(importAlertMessage) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - chooseFileWithIntent(baseIntent, IMPORT); - } - }) + @Override + public void onClick(DialogInterface dialog, int which) { + chooseFileWithIntent(baseIntent, IMPORT); + } + }) .setNegativeButton(R.string.cancel, null) .show(); }); builder.show(); } - private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) - { - ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() - { + private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) { + mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false); + ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override - public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) - { + public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) { onImportComplete(result, targetUri, dataFormat); if (closeWhenDone) { try { @@ -231,13 +222,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity importExporter = new ImportExportTask(ImportExportActivity.this, dataFormat, target, password, listener); - importExporter.execute(); + mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter); } - private void startExport(final OutputStream target, final Uri targetUri,char[] password, final boolean closeWhenDone) - { - ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() - { + private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) { + mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false); + ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) { onExportComplete(result, targetUri); @@ -252,8 +242,8 @@ public class ImportExportActivity extends CatimaAppCompatActivity }; importExporter = new ImportExportTask(ImportExportActivity.this, - DataFormat.Catima, target,password, listener); - importExporter.execute(); + DataFormat.Catima, target, password, listener); + mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter); } @Override @@ -281,22 +271,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity } @Override - protected void onDestroy() - { - if(importExporter != null && importExporter.getStatus() != AsyncTask.Status.RUNNING) - { - importExporter.cancel(true); - } + protected void onDestroy() { + mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false); + mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false); super.onDestroy(); } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); - if(id == android.R.id.home) - { + if (id == android.R.id.home) { finish(); return true; } @@ -330,13 +315,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity int messageId; - if (result == ImportExportResult.Success) - { + if (result == ImportExportResult.Success) { builder.setTitle(R.string.importSuccessfulTitle); messageId = R.string.importSuccessful; - } - else - { + } else { builder.setTitle(R.string.importFailedTitle); messageId = R.string.importFailed; } @@ -344,11 +326,9 @@ public class ImportExportActivity extends CatimaAppCompatActivity final String message = getResources().getString(messageId); builder.setMessage(message); - builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() - { + builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) - { + public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); @@ -356,19 +336,15 @@ public class ImportExportActivity extends CatimaAppCompatActivity builder.create().show(); } - private void onExportComplete(ImportExportResult result, final Uri path) - { + private void onExportComplete(ImportExportResult result, final Uri path) { AlertDialog.Builder builder = new AlertDialog.Builder(this); int messageId; - if(result == ImportExportResult.Success) - { + if (result == ImportExportResult.Success) { builder.setTitle(R.string.exportSuccessfulTitle); messageId = R.string.exportSuccessful; - } - else - { + } else { builder.setTitle(R.string.exportFailedTitle); messageId = R.string.exportFailed; } @@ -378,8 +354,7 @@ public class ImportExportActivity extends CatimaAppCompatActivity builder.setMessage(message); builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss()); - if(result == ImportExportResult.Success) - { + if (result == ImportExportResult.Success) { final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel); builder.setPositiveButton(sendLabel, (dialog, which) -> { @@ -400,58 +375,42 @@ public class ImportExportActivity extends CatimaAppCompatActivity builder.create().show(); } - private void chooseFileWithIntent(Intent intent, int requestCode) - { - try - { + private void chooseFileWithIntent(Intent intent, int requestCode) { + try { startActivityForResult(intent, requestCode); - } - catch (ActivityNotFoundException e) - { + } catch (ActivityNotFoundException e) { Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show(); Log.e(TAG, "No activity found to handle intent", e); } } private void activityResultParser(int requestCode, int resultCode, Uri uri, char[] password) { - if (resultCode != RESULT_OK) - { + if (resultCode != RESULT_OK) { Log.w(TAG, "Failed onActivityResult(), result=" + resultCode); return; } - if(uri == null) - { + if (uri == null) { Log.e(TAG, "Activity returned a NULL URI"); return; } - try - { - if (requestCode == CHOOSE_EXPORT_LOCATION) - { + try { + if (requestCode == CHOOSE_EXPORT_LOCATION) { OutputStream writer; - if (uri.getScheme() != null) - { + if (uri.getScheme() != null) { writer = getContentResolver().openOutputStream(uri); - } - else - { + } else { writer = new FileOutputStream(new File(uri.toString())); } Log.e(TAG, "Starting file export with: " + uri.toString()); - startExport(writer, uri,exportPassword.toCharArray(),true); - } - else - { + startExport(writer, uri, exportPassword.toCharArray(), true); + } else { InputStream reader; - if(uri.getScheme() != null) - { + if (uri.getScheme() != null) { reader = getContentResolver().openInputStream(uri); - } - else - { + } else { reader = new FileInputStream(new File(uri.toString())); } @@ -459,28 +418,21 @@ public class ImportExportActivity extends CatimaAppCompatActivity startImport(reader, uri, importDataFormat, password, true); } - } - catch(IOException e) - { + } catch (IOException e) { Log.e(TAG, "Failed to import/export file: " + uri.toString(), e); - if (requestCode == CHOOSE_EXPORT_LOCATION) - { + if (requestCode == CHOOSE_EXPORT_LOCATION) { onExportComplete(ImportExportResult.GenericFailure, uri); - } - else - { + } else { onImportComplete(ImportExportResult.GenericFailure, uri, importDataFormat); } } } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) - { + protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if(data == null) - { + if (data == null) { Log.e(TAG, "Activity returned NULL data"); return; } diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index df2d26e0d..d54c91b4a 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -4,7 +4,6 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.AsyncTask; import android.util.Log; import java.io.IOException; @@ -13,13 +12,13 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import protect.card_locker.async.CompatCallable; import protect.card_locker.importexport.DataFormat; import protect.card_locker.importexport.ImportExportResult; import protect.card_locker.importexport.MultiFormatExporter; import protect.card_locker.importexport.MultiFormatImporter; -class ImportExportTask extends AsyncTask -{ +public class ImportExportTask implements CompatCallable { private static final String TAG = "Catima"; private Activity activity; @@ -35,9 +34,8 @@ class ImportExportTask extends AsyncTask /** * Constructor which will setup a task for exporting to the given file */ - ImportExportTask(Activity activity, DataFormat format, OutputStream output,char[] password, - TaskCompleteListener listener) - { + ImportExportTask(Activity activity, DataFormat format, OutputStream output, char[] password, + TaskCompleteListener listener) { super(); this.activity = activity; this.doImport = false; @@ -51,8 +49,7 @@ class ImportExportTask extends AsyncTask * Constructor which will setup a task for importing from the given InputStream. */ ImportExportTask(Activity activity, DataFormat format, InputStream input, char[] password, - TaskCompleteListener listener) - { + TaskCompleteListener listener) { super(); this.activity = activity; this.doImport = true; @@ -62,8 +59,7 @@ class ImportExportTask extends AsyncTask this.listener = listener; } - private ImportExportResult performImport(Context context, InputStream stream, DBHelper db, char[] password) - { + private ImportExportResult performImport(Context context, InputStream stream, DBHelper db, char[] password) { ImportExportResult importResult = MultiFormatImporter.importData(context, db, stream, format, password); Log.i(TAG, "Import result: " + importResult.name()); @@ -71,18 +67,14 @@ class ImportExportTask extends AsyncTask return importResult; } - private ImportExportResult performExport(Context context, OutputStream stream, DBHelper db,char[] password) - { + private ImportExportResult performExport(Context context, OutputStream stream, DBHelper db, char[] password) { ImportExportResult result = ImportExportResult.GenericFailure; - try - { + try { OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8); - result = MultiFormatExporter.exportData(context, db, stream, format,password); + result = MultiFormatExporter.exportData(context, db, stream, format, password); writer.close(); - } - catch (IOException e) - { + } catch (IOException e) { Log.e(TAG, "Unable to export file", e); } @@ -91,55 +83,55 @@ class ImportExportTask extends AsyncTask return result; } - protected void onPreExecute() - { + public void onPreExecute() { progress = new ProgressDialog(activity); progress.setTitle(doImport ? R.string.importing : R.string.exporting); - progress.setOnDismissListener(new DialogInterface.OnDismissListener() - { + progress.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override - public void onDismiss(DialogInterface dialog) - { - ImportExportTask.this.cancel(true); + public void onDismiss(DialogInterface dialog) { + ImportExportTask.this.stop(); } }); progress.show(); } - protected ImportExportResult doInBackground(Void... nothing) - { + protected ImportExportResult doInBackground(Void... nothing) { final DBHelper db = new DBHelper(activity); ImportExportResult result; - if(doImport) - { + if (doImport) { result = performImport(activity.getApplicationContext(), inputStream, db, password); - } - else - { - result = performExport(activity.getApplicationContext(), outputStream, db,password); + } else { + result = performExport(activity.getApplicationContext(), outputStream, db, password); } return result; } - protected void onPostExecute(ImportExportResult result) - { - listener.onTaskComplete(result, format); + public void onPostExecute(Object castResult) { + listener.onTaskComplete((ImportExportResult) castResult, format); progress.dismiss(); Log.i(TAG, (doImport ? "Import" : "Export") + " Complete"); } - protected void onCancelled() - { + protected void onCancelled() { progress.dismiss(); Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled"); } - interface TaskCompleteListener - { + + protected void stop() { + // Whelp + } + + @Override + public ImportExportResult call() { + return doInBackground(); + } + + interface TaskCompleteListener { void onTaskComplete(ImportExportResult result, DataFormat format); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index 15791d381..430ac5657 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -3,7 +3,6 @@ package protect.card_locker; import android.content.Context; import android.database.Cursor; import android.graphics.Color; -import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.util.SparseBooleanArray; import android.view.HapticFeedbackConstants; @@ -21,11 +20,13 @@ import java.math.BigDecimal; import java.text.DateFormat; import java.util.ArrayList; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; import androidx.recyclerview.widget.RecyclerView; + import protect.card_locker.preferences.Settings; -public class LoyaltyCardCursorAdapter extends BaseCursorAdapter -{ +public class LoyaltyCardCursorAdapter extends BaseCursorAdapter { private int mCurrentSelectedIndex = -1; private Cursor mCursor; Settings mSettings; @@ -36,8 +37,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter mListener.onRowClicked(inputPosition)); inputHolder.mInformationContainer.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition)); @@ -143,85 +140,68 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter getSelectedItems() - { + public ArrayList getSelectedItems() { ArrayList result = new ArrayList<>(); int i; - for(i = 0; i < mSelectedItems.size(); i++) - { + for (i = 0; i < mSelectedItems.size(); i++) { mCursor.moveToPosition(mSelectedItems.keyAt(i)); result.add(LoyaltyCard.toLoyaltyCard(mCursor)); } @@ -229,19 +209,17 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter existingGroups = db.getGroups(); List loyaltyCardGroups = db.getLoyaltyCardGroups(loyaltyCardId); @@ -604,18 +603,17 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } // Generate random header color - if(tempLoyaltyCard.headerColor == null) - { + if (tempLoyaltyCard.headerColor == null) { // Select a random color to start out with. TypedArray colors = getResources().obtainTypedArray(R.array.letter_tile_colors); - final int color = (int)(Math.random() * colors.length()); + final int color = (int) (Math.random() * colors.length()); updateTempState(LoyaltyCardField.headerColor, colors.getColor(color, Color.BLACK)); colors.recycle(); } // It can't be null because we set it in updateTempState but SpotBugs insists it can be // NP_NULL_ON_SOME_PATH: Possible null pointer dereference - if(tempLoyaltyCard.headerColor != null) { + if (tempLoyaltyCard.headerColor != null) { thumbnail.setOnClickListener(new ColorSelectListener()); } @@ -806,11 +804,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity startActivityForResult(i, type); } - class EditCardIdAndBarcode implements View.OnClickListener - { + class EditCardIdAndBarcode implements View.OnClickListener { @Override - public void onClick(View v) - { + public void onClick(View v) { Intent i = new Intent(getApplicationContext(), ScanActivity.class); final Bundle b = new Bundle(); b.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, cardIdFieldView.getText().toString()); @@ -819,11 +815,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } } - class ChooseCardImage implements View.OnClickListener - { + class ChooseCardImage implements View.OnClickListener { @Override - public void onClick(View v) throws NoSuchElementException - { + public void onClick(View v) throws NoSuchElementException { ImageView targetView = v.getId() == ID_IMAGE_FRONT ? cardImageFront : cardImageBack; LinkedHashMap> cardOptions = new LinkedHashMap<>(); @@ -870,11 +864,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } } - class ColorSelectListener implements View.OnClickListener - { + class ColorSelectListener implements View.OnClickListener { @Override - public void onClick(View v) - { + public void onClick(View v) { ColorPickerDialog.Builder dialogBuilder = ColorPickerDialog.newBuilder(); if (tempLoyaltyCard.headerColor != null) { @@ -882,19 +874,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } ColorPickerDialog dialog = dialogBuilder.create(); - dialog.setColorPickerDialogListener(new ColorPickerDialogListener() - { + dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { @Override - public void onColorSelected(int dialogId, int color) - { + public void onColorSelected(int dialogId, int color) { updateTempState(LoyaltyCardField.headerColor, color); generateIcon(storeFieldEdit.getText().toString()); } @Override - public void onDialogDismissed(int dialogId) - { + public void onDialogDismissed(int dialogId) { // Nothing to do, no change made } }); @@ -955,20 +944,17 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity return; } - if(tempLoyaltyCard.store.isEmpty()) - { + if (tempLoyaltyCard.store.isEmpty()) { Snackbar.make(storeFieldEdit, R.string.noStoreError, Snackbar.LENGTH_LONG).show(); return; } - if(tempLoyaltyCard.cardId.isEmpty()) - { + if (tempLoyaltyCard.cardId.isEmpty()) { Snackbar.make(cardIdFieldView, R.string.noCardIdError, Snackbar.LENGTH_LONG).show(); return; } - if(!validBalance) - { + if (!validBalance) { Snackbar.make(balanceField, getString(R.string.parsingBalanceFailed, balanceField.getText().toString()), Snackbar.LENGTH_LONG).show(); return; } @@ -980,8 +966,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity selectedGroups.add((Group) chip.getTag()); } - if(updateLoyaltyCard) - { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) + if (updateLoyaltyCard) { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) db.updateLoyaltyCard(loyaltyCardId, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor); try { Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); @@ -990,9 +975,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity e.printStackTrace(); } Log.i(TAG, "Updated " + loyaltyCardId + " to " + cardId); - } - else - { + } else { loyaltyCardId = (int) db.insertLoyaltyCard(tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, 0, tempLoyaltyCard.lastUsed); try { Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); @@ -1008,14 +991,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } @Override - public boolean onCreateOptionsMenu(Menu menu) - { - if(updateLoyaltyCard) - { + public boolean onCreateOptionsMenu(Menu menu) { + if (updateLoyaltyCard) { getMenuInflater().inflate(R.menu.card_update_menu, menu); - } - else - { + } else { getMenuInflater().inflate(R.menu.card_add_menu, menu); } @@ -1023,12 +1002,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); - switch(id) - { + switch (id) { case android.R.id.home: askBeforeQuitIfChanged(); break; @@ -1059,8 +1036,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) - { + public void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { @@ -1143,6 +1119,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity } private void generateBarcode(String cardIdString, CatimaBarcode barcodeFormat) { + mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); + if (barcodeImage.getHeight() == 0) { Log.d(TAG, "ImageView size is not known known at start, waiting for load"); // The size of the ImageView is not yet available as it has not @@ -1154,12 +1132,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this); Log.d(TAG, "ImageView size now known"); - new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, warnOnInvalidBarcodeType).execute(); + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, warnOnInvalidBarcodeType); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } }); } else { Log.d(TAG, "ImageView size known known, creating barcode"); - new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, warnOnInvalidBarcodeType).execute(); + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, warnOnInvalidBarcodeType); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } showBarcode(); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 91bbb2b2b..fa02b5ade 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -30,7 +30,6 @@ import android.widget.Toast; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.zxing.BarcodeFormat; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; @@ -48,10 +47,11 @@ import androidx.constraintlayout.widget.Guideline; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.widget.TextViewCompat; + +import protect.card_locker.async.TaskHandler; import protect.card_locker.preferences.Settings; -public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements GestureDetector.OnGestureListener -{ +public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements GestureDetector.OnGestureListener { private static final String TAG = "Catima"; private GestureDetector mGestureDetector; @@ -102,6 +102,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements static final String STATE_IMAGEINDEX = "imageIndex"; static final String STATE_FULLSCREEN = "isFullscreen"; + final private TaskHandler mTasks = new TaskHandler(); + @Override public boolean onDown(MotionEvent e) { return true; @@ -182,8 +184,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements IMAGE_BACK } - private void extractIntentFields(Intent intent) - { + private void extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); loyaltyCardId = b != null ? b.getInt("id") : 0; Log.d(TAG, "View activity: id=" + loyaltyCardId); @@ -198,17 +199,13 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return wrappedIcon; } - private Drawable getIcon(int icon, boolean dark) - { + private Drawable getIcon(int icon, boolean dark) { Drawable unwrappedIcon = AppCompatResources.getDrawable(this, icon); assert unwrappedIcon != null; Drawable wrappedIcon = DrawableCompat.wrap(unwrappedIcon); - if(dark) - { + if (dark) { DrawableCompat.setTint(wrappedIcon, Color.BLACK); - } - else - { + } else { DrawableCompat.setTintList(wrappedIcon, null); } @@ -216,8 +213,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { @@ -268,10 +264,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements if (imageTypes.get(mainImageIndex) == ImageType.BARCODE) { redrawBarcodeAfterResize(); } - if(loyaltyCard!=null && format!=null && format.isSquare()) + if (loyaltyCard != null && format != null && format.isSquare()) { centerGuideline.setGuidelinePercent(0.75f * scale); - else + } else { centerGuideline.setGuidelinePercent(0.5f * scale); + } } @Override @@ -324,7 +321,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { } + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + } }); bottomSheetButton.setOnClickListener(v -> { @@ -360,8 +358,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } @Override - public void onNewIntent(Intent intent) - { + public void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.i(TAG, "Received new intent"); @@ -377,8 +374,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements @Override - public void onResume() - { + public void onResume() { super.onResume(); Log.i(TAG, "To view card: " + loyaltyCardId); @@ -387,12 +383,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements // '1' is the brightest. We attempt to maximize the brightness // to help barcode readers scan the barcode. Window window = getWindow(); - if(window != null) - { + if (window != null) { WindowManager.LayoutParams attributes = window.getAttributes(); - if (settings.useMaxBrightnessDisplayingBarcode()) - { + if (settings.useMaxBrightnessDisplayingBarcode()) { attributes.screenBrightness = 1F; } @@ -401,7 +395,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } if (settings.getDisableLockscreenWhileViewingCard()) { - window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD| + window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); } @@ -409,8 +403,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } loyaltyCard = db.getLoyaltyCard(loyaltyCardId); - if(loyaltyCard == null) - { + if (loyaltyCard == null) { Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); finish(); @@ -420,10 +413,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements setupOrientation(); format = loyaltyCard.barcodeType; - if(format != null && format.isSquare()){ + if (format != null && format.isSquare()) { centerGuideline.setGuidelinePercent(0.75f); - } - else{ + } else { centerGuideline.setGuidelinePercent(0.5f); } cardIdString = loyaltyCard.cardId; @@ -434,20 +426,17 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements settings.getFontSizeMin(settings.getLargeFont()), settings.getFontSizeMax(settings.getLargeFont()), 1, TypedValue.COMPLEX_UNIT_SP); - if(loyaltyCard.note.length() > 0) - { + if (loyaltyCard.note.length() > 0) { noteView.setVisibility(View.VISIBLE); noteView.setText(loyaltyCard.note); noteView.setTextSize(settings.getFontSizeMax(settings.getMediumFont())); - } - else - { + } else { noteView.setVisibility(View.GONE); } List loyaltyCardGroups = db.getLoyaltyCardGroups(loyaltyCardId); - if(loyaltyCardGroups.size() > 0) { + if (loyaltyCardGroups.size() > 0) { List groupNames = new ArrayList<>(); for (Group group : loyaltyCardGroups) { groupNames.add(group._id); @@ -456,35 +445,29 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements groupsView.setVisibility(View.VISIBLE); groupsView.setText(getString(R.string.groupsList, TextUtils.join(", ", groupNames))); groupsView.setTextSize(settings.getFontSizeMax(settings.getMediumFont())); - } - else - { + } else { groupsView.setVisibility(View.GONE); } - if(!loyaltyCard.balance.equals(new BigDecimal(0))) { + if (!loyaltyCard.balance.equals(new BigDecimal(0))) { balanceView.setVisibility(View.VISIBLE); balanceView.setText(getString(R.string.balanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType))); balanceView.setTextSize(settings.getFontSizeMax(settings.getMediumFont())); - } - else - { + } else { balanceView.setVisibility(View.GONE); } - if(loyaltyCard.expiry != null) { + if (loyaltyCard.expiry != null) { expiryView.setVisibility(View.VISIBLE); int expiryString = R.string.expiryStateSentence; - if(Utils.hasExpired(loyaltyCard.expiry)) { + if (Utils.hasExpired(loyaltyCard.expiry)) { expiryString = R.string.expiryStateSentenceExpired; expiryView.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.alert)); } expiryView.setText(getString(expiryString, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry))); expiryView.setTextSize(settings.getFontSizeMax(settings.getMediumFont())); - } - else - { + } else { expiryView.setVisibility(View.GONE); } expiryView.setTag(loyaltyCard.expiry); @@ -503,12 +486,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements TypedValue.COMPLEX_UNIT_DIP); int backgroundHeaderColor; - if(loyaltyCard.headerColor != null) - { + if (loyaltyCard.headerColor != null) { backgroundHeaderColor = loyaltyCard.headerColor; - } - else - { + } else { backgroundHeaderColor = LetterBitmap.getDefaultColor(this, loyaltyCard.store); } @@ -516,12 +496,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements appBarLayout.setBackgroundColor(backgroundHeaderColor); int textColor; - if(Utils.needsDarkForeground(backgroundHeaderColor)) - { + if (Utils.needsDarkForeground(backgroundHeaderColor)) { textColor = Color.BLACK; - } - else - { + } else { textColor = Color.WHITE; } storeName.setTextColor(textColor); @@ -530,14 +507,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements // If the background is very bright, we should use dark icons backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setHomeAsUpIndicator(getIcon(R.drawable.ic_arrow_back_white, backgroundNeedsDarkIcons)); } // Make notification area light if dark icons are needed - if(Build.VERSION.SDK_INT >= 23) - { + if (Build.VERSION.SDK_INT >= 23) { window.getDecorView().setSystemUiVisibility(backgroundNeedsDarkIcons ? View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : 0); } window.setStatusBarColor(Color.TRANSPARENT); @@ -594,8 +569,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements @Override public void onBackPressed() { - if (isFullscreen) - { + if (isFullscreen) { setFullscreen(false); return; } @@ -604,16 +578,14 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } @Override - public boolean onCreateOptionsMenu(Menu menu) - { + public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.card_view_menu, menu); // Always calculate lockscreen icon, it may need a black color boolean lockBarcodeScreenOrientation = settings.getLockBarcodeScreenOrientation(); MenuItem item = menu.findItem(R.id.action_lock_unlock); setOrientatonLock(item, lockBarcodeScreenOrientation); - if(lockBarcodeScreenOrientation) - { + if (lockBarcodeScreenOrientation) { item.setVisible(false); } @@ -632,8 +604,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements if (starred) { menu.findItem(R.id.action_star_unstar).setIcon(getIcon(R.drawable.ic_starred_white, backgroundNeedsDarkIcons)); menu.findItem(R.id.action_star_unstar).setTitle(R.string.unstar); - } - else { + } else { menu.findItem(R.id.action_star_unstar).setIcon(getIcon(R.drawable.ic_unstarred_white, backgroundNeedsDarkIcons)); menu.findItem(R.id.action_star_unstar).setTitle(R.string.star); } @@ -641,12 +612,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); - switch(id) - { + switch (id) { case android.R.id.home: finish(); break; @@ -661,12 +630,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return true; case R.id.action_lock_unlock: - if(rotationEnabled) - { + if (rotationEnabled) { setOrientatonLock(item, true); - } - else - { + } else { setOrientatonLock(item, false); } rotationEnabled = !rotationEnabled; @@ -682,8 +648,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return super.onOptionsItemSelected(item); } - private void setupOrientation() - { + private void setupOrientation() { Toolbar portraitToolbar = findViewById(R.id.toolbar); Toolbar landscapeToolbar = findViewById(R.id.toolbar_landscape); @@ -711,51 +676,44 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } - private void setOrientatonLock(MenuItem item, boolean lock) - { - if(lock) - { + private void setOrientatonLock(MenuItem item, boolean lock) { + if (lock) { item.setIcon(getIcon(R.drawable.ic_lock_outline_white_24dp, backgroundNeedsDarkIcons)); item.setTitle(R.string.unlockScreen); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); - } - else - { + } else { item.setIcon(getIcon(R.drawable.ic_lock_open_white_24dp, backgroundNeedsDarkIcons)); item.setTitle(R.string.lockScreen); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } - private void makeBottomSheetVisibleIfUseful() - { + private void makeBottomSheetVisibleIfUseful() { if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) { bottomSheet.setVisibility(View.VISIBLE); - } - else - { + } else { bottomSheet.setVisibility(View.GONE); } } private void drawBarcode() { + mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); if (format != null) { - new BarcodeImageWriterTask( + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask( getApplicationContext(), mainImage, barcodeIdString != null ? barcodeIdString : cardIdString, format, null, false, - null) - .execute(); + null); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } } @@ -771,7 +729,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements drawBarcode(); } }); - }; + } } private void drawMainImage(int index, boolean waitForResize) { @@ -844,8 +802,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements barcodeScaler.setVisibility(View.VISIBLE); // Hide actionbar - if(actionBar != null) - { + if (actionBar != null) { actionBar.hide(); } @@ -870,9 +827,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN ); - } - else - { + } else { Log.d(TAG, "Move out of fullscreen"); // Reset center guideline @@ -887,8 +842,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements barcodeScaler.setVisibility(View.GONE); // Show actionbar - if(actionBar != null) - { + if (actionBar != null) { actionBar.show(); } diff --git a/app/src/main/java/protect/card_locker/async/CompatCallable.java b/app/src/main/java/protect/card_locker/async/CompatCallable.java new file mode 100644 index 000000000..e58d15c2c --- /dev/null +++ b/app/src/main/java/protect/card_locker/async/CompatCallable.java @@ -0,0 +1,8 @@ +package protect.card_locker.async; + +import java.util.concurrent.Callable; + +public interface CompatCallable extends Callable { + void onPostExecute(Object result); + void onPreExecute(); +} diff --git a/app/src/main/java/protect/card_locker/async/TaskHandler.java b/app/src/main/java/protect/card_locker/async/TaskHandler.java new file mode 100644 index 000000000..c25b13e33 --- /dev/null +++ b/app/src/main/java/protect/card_locker/async/TaskHandler.java @@ -0,0 +1,174 @@ +package protect.card_locker.async; + +import android.os.Handler; +import android.os.Looper; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * AsyncTask has been deprecated so this provides very rudimentary compatibility without + * needing to redo too many Parts. + * + * However this is a much, much more cooperative Behaviour than before so + * the callers need to ensure we do NOT rely on forced cancellation and feed less into the + * ThreadPools so we don't OOM/Overload the Users device + * + * This assumes single-threaded callers. + */ +public class TaskHandler { + + public enum TYPE { + BARCODE, + IMPORT, + EXPORT + } + + HashMap executors = generateExecutors(); + + final private HashMap>> taskList = new HashMap<>(); + + private final Handler uiHandler = new Handler(Looper.getMainLooper()); + + private HashMap generateExecutors() { + HashMap initExecutors = new HashMap<>(); + for (TYPE type : TYPE.values()) { + replaceExecutor(initExecutors, type, false, false); + } + return initExecutors; + } + + /** + * Replaces (or initializes) an Executor with a clean (new) one + * @param executors Map Reference + * @param type Which Queue + * @param flushOld attempt shutdown + * @param waitOnOld wait for Termination + */ + private void replaceExecutor(HashMap executors, TYPE type, Boolean flushOld, Boolean waitOnOld) { + ThreadPoolExecutor oldExecutor = executors.get(type); + if (oldExecutor != null) { + if (flushOld) { + oldExecutor.shutdownNow(); + } + if (waitOnOld) { + try { + //noinspection ResultOfMethodCallIgnored + oldExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + executors.put(type, (ThreadPoolExecutor) Executors.newCachedThreadPool()); + } + + /** + * Queue a Pseudo-AsyncTask for execution + * + * @param type Queue + * @param callable PseudoAsyncTask + */ + public void executeTask(TYPE type, CompatCallable callable) { + Runnable runner = () -> { + try { + // Run on the UI Thread + uiHandler.post(callable::onPreExecute); + + // Background + final Object result = callable.call(); + + // Post results on UI Thread so we can show them + uiHandler.post(() -> { + callable.onPostExecute(result); + }); + } catch (Exception e) { + e.printStackTrace(); + } + }; + + LinkedList> list = taskList.get(type); + + if (list == null) { + list = new LinkedList<>(); + } + + ThreadPoolExecutor executor = executors.get(type); + + if (executor != null) { + Future task = executor.submit(runner); + // Test Queue Cancellation: + // task.cancel(true); + list.push(task); + taskList.put(type, list); + } + } + + /** + * This will attempt to cancel a currently running list of Tasks + * Useful to ignore scheduled tasks - but not able to hard-stop tasks that are running + * + * @param type Which Queue to target + * @param forceCancel attempt to close the Queue and force-replace it after + * @param waitForFinish wait and return after the old executor finished. Times out after 5s + * @param waitForCurrentlyRunning wait before cancelling tasks. Useful for tests. + */ + public void flushTaskList(TYPE type, Boolean forceCancel, Boolean waitForFinish, Boolean waitForCurrentlyRunning) { + // Only used for Testing + if (waitForCurrentlyRunning) { + ThreadPoolExecutor oldExecutor = executors.get(type); + + if (oldExecutor != null) { + try { + //noinspection ResultOfMethodCallIgnored + oldExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + // Attempt to cancel known Tasks and clean the List + LinkedList> tasks = taskList.get(type); + if (tasks != null) { + for (Future task : tasks) { + if (!task.isDone() || !task.isCancelled()) { + // Interrupt any Task we can + task.cancel(true); + } + } + } + tasks = new LinkedList<>(); + taskList.put(type, tasks); + + if (forceCancel || waitForFinish) { + ThreadPoolExecutor oldExecutor = executors.get(type); + + if (oldExecutor != null) { + if (forceCancel) { + if (waitForFinish) { + try { + //noinspection ResultOfMethodCallIgnored + oldExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + oldExecutor.shutdownNow(); + replaceExecutor(executors, type, true, false); + } else { + try { + //noinspection ResultOfMethodCallIgnored + oldExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 0c42f5210..2ea858e1f 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -104,7 +104,7 @@ public class CatimaImporter implements Importer public void parseV1(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException { - final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.withHeader()); + final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.builder().setHeader().build()); SQLiteDatabase database = db.getWritableDatabase(); database.beginTransaction(); @@ -209,7 +209,7 @@ public class CatimaImporter implements Importer public void parseV2Groups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse groups - final CSVParser groupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.withHeader()); + final CSVParser groupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); List records = new ArrayList<>(); @@ -235,7 +235,7 @@ public class CatimaImporter implements Importer public void parseV2Cards(Context context, DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse cards - final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.withHeader()); + final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); List records = new ArrayList<>(); @@ -261,7 +261,7 @@ public class CatimaImporter implements Importer public void parseV2CardGroups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse card group mappings - final CSVParser cardGroupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.withHeader()); + final CSVParser cardGroupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); List records = new ArrayList<>(); diff --git a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java index b41983b5f..f5453db67 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -32,8 +32,7 @@ import protect.card_locker.Utils; * The database's loyalty cards are expected to appear in the CSV data. * A header is expected for the each table showing the names of the columns. */ -public class FidmeImporter implements Importer -{ +public class FidmeImporter implements Importer { public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException { // We actually retrieve a .zip file ZipInputStream zipInputStream = new ZipInputStream(input, password); @@ -59,7 +58,7 @@ public class FidmeImporter implements Importer SQLiteDatabase database = db.getWritableDatabase(); database.beginTransaction(); - final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.withDelimiter(';').withHeader()); + final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.builder().setDelimiter(';').setHeader().build()); try { for (CSVRecord record : fidmeParser) { @@ -87,8 +86,7 @@ public class FidmeImporter implements Importer * session. */ private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record) - throws IOException, FormatException - { + throws IOException, FormatException { // A loyalty card export from Fidme contains the following fields: // Retailer (store name) // Program (program name) @@ -100,8 +98,7 @@ public class FidmeImporter implements Importer // The store is called Retailer String store = CSVHelpers.extractString("Retailer", record, ""); - if (store.isEmpty()) - { + if (store.isEmpty()) { throw new FormatException("No store listed, but is required"); } @@ -121,8 +118,7 @@ public class FidmeImporter implements Importer // The ID is called reference String cardId = CSVHelpers.extractString("Reference", record, ""); - if(cardId.isEmpty()) - { + if (cardId.isEmpty()) { throw new FormatException("No card ID listed, but is required"); } diff --git a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java index 63b4bd844..76761b83a 100644 --- a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java @@ -43,7 +43,7 @@ public class StocardImporter implements Importer HashMap> loyaltyCardHashMap = new HashMap<>(); HashMap> providers = new HashMap<>(); - final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.withHeader()); + final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.builder().setHeader().build()); try { diff --git a/app/src/test/java/protect/card_locker/BarcodeSelectorActivityTest.java b/app/src/test/java/protect/card_locker/BarcodeSelectorActivityTest.java index 993355c56..c205dc5bc 100644 --- a/app/src/test/java/protect/card_locker/BarcodeSelectorActivityTest.java +++ b/app/src/test/java/protect/card_locker/BarcodeSelectorActivityTest.java @@ -2,7 +2,6 @@ package protect.card_locker; import android.app.Activity; import android.content.Intent; -import android.os.Looper; import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -13,16 +12,19 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.robolectric.Shadows.shadowOf; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 23) public class BarcodeSelectorActivityTest { @Test - public void emptyStateTest() - { + public void emptyStateTest() { ActivityController activityController = Robolectric.buildActivity(BarcodeSelectorActivity.class).create(); activityController.start(); activityController.resume(); @@ -41,8 +43,7 @@ public class BarcodeSelectorActivityTest { } @Test - public void nonEmptyStateTest() throws InterruptedException - { + public void nonEmptyStateTest() throws InterruptedException { ActivityController activityController = Robolectric.buildActivity(BarcodeSelectorActivity.class).create(); activityController.start(); activityController.resume(); @@ -54,11 +55,12 @@ public class BarcodeSelectorActivityTest { cardId.setText("abcdefg"); - shadowOf(Looper.getMainLooper()).idle(); + // Run the delayed Handler + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); // Button should be visible and enabled assertEquals(View.VISIBLE, noBarcodeButton.getVisibility()); - assertEquals(true, noBarcodeButton.isEnabled()); + assertTrue(noBarcodeButton.isEnabled()); // Clicking button should create "empty" barcode activity.findViewById(R.id.noBarcode).performClick(); @@ -70,8 +72,7 @@ public class BarcodeSelectorActivityTest { } @Test - public void nonEmptyToEmptyStateTest() throws InterruptedException - { + public void nonEmptyToEmptyStateTest() throws InterruptedException { ActivityController activityController = Robolectric.buildActivity(BarcodeSelectorActivity.class).create(); activityController.start(); activityController.resume(); @@ -83,18 +84,20 @@ public class BarcodeSelectorActivityTest { cardId.setText("abcdefg"); - shadowOf(Looper.getMainLooper()).idle(); + // Run the delayed Handler + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); // Button should be visible and enabled assertEquals(View.VISIBLE, noBarcodeButton.getVisibility()); - assertEquals(true, noBarcodeButton.isEnabled()); + assertTrue(noBarcodeButton.isEnabled()); cardId.setText(""); - shadowOf(Looper.getMainLooper()).idle(); + // Run the delayed Handler + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); // Button should be visible but disabled assertEquals(View.VISIBLE, noBarcodeButton.getVisibility()); - assertEquals(false, noBarcodeButton.isEnabled()); + assertFalse(noBarcodeButton.isEnabled()); } } diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 230faef6f..48c2d9fdc 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -7,6 +7,7 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.os.Environment; +import android.os.Looper; import android.util.DisplayMetrics; import com.google.zxing.BarcodeFormat; @@ -20,6 +21,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowLog; +import org.robolectric.shadows.ShadowLooper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -33,6 +35,7 @@ import java.io.OutputStreamWriter; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.text.ParseException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -42,6 +45,8 @@ import java.util.HashMap; import java.util.List; import androidx.core.content.res.ResourcesCompat; + +import protect.card_locker.async.TaskHandler; import protect.card_locker.importexport.DataFormat; import protect.card_locker.importexport.ImportExportResult; import protect.card_locker.importexport.MultiFormatExporter; @@ -51,11 +56,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.robolectric.Shadows.shadowOf; @RunWith(RobolectricTestRunner.class) @Config(sdk = 23) -public class ImportExportTest -{ +public class ImportExportTest { private Activity activity; private DBHelper db; private long nowMs; @@ -66,8 +71,7 @@ public class ImportExportTest private final CatimaBarcode BARCODE_TYPE = CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A); @Before - public void setUp() - { + public void setUp() { ShadowLog.stream = System.out; activity = Robolectric.setupActivity(MainActivity.class); @@ -75,20 +79,19 @@ public class ImportExportTest nowMs = System.currentTimeMillis(); Calendar lastYear = Calendar.getInstance(); - lastYear.set(Calendar.YEAR, lastYear.get(Calendar.YEAR)-1); + lastYear.set(Calendar.YEAR, lastYear.get(Calendar.YEAR) - 1); lastYearMs = lastYear.getTimeInMillis(); } /** * Add the given number of cards, each with * an index in the store name. + * * @param cardsToAdd */ - private void addLoyaltyCards(int cardsToAdd) - { + private void addLoyaltyCards(int cardsToAdd) { // Add in reverse order to test sorting - for(int index = cardsToAdd; index > 0; index--) - { + for (int index = cardsToAdd; index > 0; index--) { String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); long id = db.insertLoyaltyCard(storeName, note, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 0, null); @@ -99,20 +102,17 @@ public class ImportExportTest assertEquals(cardsToAdd, db.getLoyaltyCardCount()); } - private void addLoyaltyCardsFiveStarred() - { + private void addLoyaltyCardsFiveStarred() { int cardsToAdd = 9; // Add in reverse order to test sorting - for(int index = cardsToAdd; index > 4; index--) - { + for (int index = cardsToAdd; index > 4; index--) { String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); long id = db.insertLoyaltyCard(storeName, note, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, null, BARCODE_TYPE, index, 1, null); boolean result = (id != -1); assertTrue(result); } - for(int index = cardsToAdd-5; index > 0; index--) - { + for (int index = cardsToAdd - 5; index > 0; index--) { String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); //if index is even @@ -124,8 +124,7 @@ public class ImportExportTest } @Test - public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() - { + public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { long id = db.insertLoyaltyCard("No Expiry", "", null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, 0, 0, null); boolean result = (id != -1); assertTrue(result); @@ -165,8 +164,8 @@ public class ImportExportTest card = db.getLoyaltyCard((int) id); assertEquals("Today", card.store); assertEquals("", card.note); - assertTrue(card.expiry.before(new Date(new Date().getTime()+86400))); - assertTrue(card.expiry.after(new Date(new Date().getTime()-86400))); + assertTrue(card.expiry.before(new Date(new Date().getTime() + 86400))); + assertTrue(card.expiry.after(new Date(new Date().getTime() - 86400))); assertEquals(new BigDecimal("0"), card.balance); assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); @@ -184,7 +183,7 @@ public class ImportExportTest card = db.getLoyaltyCard((int) id); assertEquals("Future", card.store); assertEquals("", card.note); - assertTrue(card.expiry.after(new Date(new Date().getTime()+86400))); + assertTrue(card.expiry.after(new Date(new Date().getTime() + 86400))); assertEquals(new BigDecimal("0"), card.balance); assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); @@ -196,11 +195,9 @@ public class ImportExportTest assertEquals(4, db.getLoyaltyCardCount()); } - private void addGroups(int groupsToAdd) - { + private void addGroups(int groupsToAdd) { // Add in reverse order to test sorting - for(int index = groupsToAdd; index > 0; index--) - { + for (int index = groupsToAdd; index > 0; index--) { String groupName = String.format("group, \"%4d", index); long id = db.insertGroup(groupName); boolean result = (id != -1); @@ -215,13 +212,11 @@ public class ImportExportTest * specified in addLoyaltyCards(), and are in sequential order * where the smallest card's index is 1 */ - private void checkLoyaltyCards() - { + private void checkLoyaltyCards() { Cursor cursor = db.getLoyaltyCardCursor(); int index = 1; - while(cursor.moveToNext()) - { + while (cursor.moveToNext()) { LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); String expectedStore = String.format("store, \"%4d", index); @@ -248,13 +243,11 @@ public class ImportExportTest * specified in addLoyaltyCardsSomeStarred(), and are in sequential order * with starred ones first */ - private void checkLoyaltyCardsFiveStarred() - { - Cursor cursor = db.getLoyaltyCardCursor(); - int index = 5; + private void checkLoyaltyCardsFiveStarred() { + Cursor cursor = db.getLoyaltyCardCursor(); + int index = 5; - while(index<10) - { + while (index < 10) { cursor.moveToNext(); LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); @@ -276,26 +269,25 @@ public class ImportExportTest } index = 1; - while(cursor.moveToNext() && index<5) - { - LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); + while (cursor.moveToNext() && index < 5) { + LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); - String expectedStore = String.format("store, \"%4d", index); - String expectedNote = String.format("note, \"%4d", index); + String expectedStore = String.format("store, \"%4d", index); + String expectedNote = String.format("note, \"%4d", index); - assertEquals(expectedStore, card.store); - assertEquals(expectedNote, card.note); - assertEquals(null, card.expiry); - assertEquals(new BigDecimal(String.valueOf(index)), card.balance); - assertEquals(null, card.balanceType); - assertEquals(BARCODE_DATA, card.cardId); - assertEquals(null, card.barcodeId); - assertEquals(BARCODE_TYPE.format(), card.barcodeType.format()); - assertEquals(Integer.valueOf(index), card.headerColor); - assertEquals(0, card.starStatus); + assertEquals(expectedStore, card.store); + assertEquals(expectedNote, card.note); + assertEquals(null, card.expiry); + assertEquals(new BigDecimal(String.valueOf(index)), card.balance); + assertEquals(null, card.balanceType); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(null, card.barcodeId); + assertEquals(BARCODE_TYPE.format(), card.barcodeType.format()); + assertEquals(Integer.valueOf(index), card.headerColor); + assertEquals(0, card.starStatus); - index++; - } + index++; + } cursor.close(); } @@ -305,13 +297,11 @@ public class ImportExportTest * specified in addGroups(), and are in sequential order * where the smallest group's index is 1 */ - private void checkGroups() - { + private void checkGroups() { Cursor cursor = db.getGroupCursor(); int index = db.getGroupCount(); - while(cursor.moveToNext()) - { + while (cursor.moveToNext()) { Group group = Group.toGroup(cursor); String expectedGroupName = String.format("group, \"%4d", index); @@ -324,8 +314,7 @@ public class ImportExportTest } @Test - public void multipleCardsExportImport() throws IOException - { + public void multipleCardsExportImport() throws IOException { final int NUM_CARDS = 10; addLoyaltyCards(NUM_CARDS); @@ -334,7 +323,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,null); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, null); assertEquals(ImportExportResult.Success, result); outStream.close(); @@ -354,18 +343,17 @@ public class ImportExportTest TestHelpers.getEmptyDb(activity); } - public void multipleCardsExportImportPasswordProtected() throws IOException - { + public void multipleCardsExportImportPasswordProtected() throws IOException { final int NUM_CARDS = 10; List passwords = Arrays.asList(null, "123456789".toCharArray()); - for(char[] password : passwords){ + for (char[] password : passwords) { addLoyaltyCards(NUM_CARDS); ByteArrayOutputStream outData = new ByteArrayOutputStream(); OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,password); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, password); assertEquals(ImportExportResult.Success, result); outStream.close(); @@ -388,8 +376,7 @@ public class ImportExportTest } @Test - public void multipleCardsExportImportSomeStarred() throws IOException - { + public void multipleCardsExportImportSomeStarred() throws IOException { final int NUM_CARDS = 9; addLoyaltyCardsFiveStarred(); @@ -398,7 +385,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,null); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, null); assertEquals(ImportExportResult.Success, result); outStream.close(); @@ -418,8 +405,7 @@ public class ImportExportTest TestHelpers.getEmptyDb(activity); } - private List groupsToGroupNames(List groups) - { + private List groupsToGroupNames(List groups) { List groupNames = new ArrayList<>(); for (Group group : groups) { @@ -430,8 +416,7 @@ public class ImportExportTest } @Test - public void multipleCardsExportImportWithGroups() throws IOException - { + public void multipleCardsExportImportWithGroups() throws IOException { final int NUM_CARDS = 10; final int NUM_GROUPS = 3; @@ -471,7 +456,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,null); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, null); assertEquals(ImportExportResult.Success, result); outStream.close(); @@ -505,8 +490,7 @@ public class ImportExportTest } @Test - public void importExistingCardsNotReplace() throws IOException - { + public void importExistingCardsNotReplace() throws IOException { final int NUM_CARDS = 10; addLoyaltyCards(NUM_CARDS); @@ -515,7 +499,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export into CSV data - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,null); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, null); assertEquals(ImportExportResult.Success, result); outStream.close(); @@ -534,19 +518,17 @@ public class ImportExportTest } @Test - public void corruptedImportNothingSaved() throws IOException - { + public void corruptedImportNothingSaved() throws IOException { final int NUM_CARDS = 10; - for(DataFormat format : DataFormat.values()) - { + for (DataFormat format : DataFormat.values()) { addLoyaltyCards(NUM_CARDS); ByteArrayOutputStream outData = new ByteArrayOutputStream(); OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima,null); + ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima, null); assertEquals(ImportExportResult.Success, result); TestHelpers.getEmptyDb(activity); @@ -569,20 +551,17 @@ public class ImportExportTest } } - class TestTaskCompleteListener implements ImportExportTask.TaskCompleteListener - { + class TestTaskCompleteListener implements ImportExportTask.TaskCompleteListener { ImportExportResult result; - public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) - { + public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) { this.result = result; } } @Test - @LooperMode(LooperMode.Mode.LEGACY) - public void useImportExportTask() throws FileNotFoundException - { + @LooperMode(LooperMode.Mode.PAUSED) + public void useImportExportTask() throws FileNotFoundException { final int NUM_CARDS = 10; final File sdcardDir = Environment.getExternalStorageDirectory(); @@ -595,11 +574,15 @@ public class ImportExportTest // Export to the file final String password = "123456789"; FileOutputStream fileOutputStream = new FileOutputStream(exportFile); - ImportExportTask task = new ImportExportTask(activity, DataFormat.Catima, fileOutputStream,password.toCharArray(), listener); - task.execute(); + ImportExportTask task = new ImportExportTask(activity, DataFormat.Catima, fileOutputStream, password.toCharArray(), listener); + TaskHandler mTasks = new TaskHandler(); + mTasks.executeTask(TaskHandler.TYPE.EXPORT, task); // Actually run the task to completion - Robolectric.flushBackgroundThreadScheduler(); + mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, false, false, true); + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5000)); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + // Check that the listener was executed assertNotNull(listener.result); @@ -614,10 +597,13 @@ public class ImportExportTest FileInputStream fileStream = new FileInputStream(exportFile); task = new ImportExportTask(activity, DataFormat.Catima, fileStream, password.toCharArray(), listener); - task.execute(); + mTasks.executeTask(TaskHandler.TYPE.IMPORT, task); // Actually run the task to completion - Robolectric.flushBackgroundThreadScheduler(); + // I am CONVINCED there must be a better way than to wait on this Queue with a flush. + mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, false, false, true); + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5000)); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); // Check that the listener was executed assertNotNull(listener.result); @@ -632,8 +618,7 @@ public class ImportExportTest } @Test - public void importWithoutColorsV1() throws IOException - { + public void importWithoutColorsV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -668,8 +653,7 @@ public class ImportExportTest } @Test - public void importWithoutNullColorsV1() throws IOException - { + public void importWithoutNullColorsV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -706,8 +690,7 @@ public class ImportExportTest } @Test - public void importWithoutInvalidColorsV1() throws IOException - { + public void importWithoutInvalidColorsV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -731,8 +714,7 @@ public class ImportExportTest } @Test - public void importWithNoBarcodeTypeV1() throws IOException - { + public void importWithNoBarcodeTypeV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -769,8 +751,7 @@ public class ImportExportTest } @Test - public void importWithStarredFieldV1() throws IOException - { + public void importWithStarredFieldV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -807,8 +788,7 @@ public class ImportExportTest } @Test - public void importWithNoStarredFieldV1() throws IOException - { + public void importWithNoStarredFieldV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -845,8 +825,7 @@ public class ImportExportTest } @Test - public void importWithInvalidStarFieldV1() throws IOException - { + public void importWithInvalidStarFieldV1() throws IOException { String csvText = ""; csvText += DBHelper.LoyaltyCardDbIds.ID + "," + DBHelper.LoyaltyCardDbIds.STORE + "," + @@ -902,8 +881,7 @@ public class ImportExportTest } @Test - public void exportImportV2Zip() throws FileNotFoundException - { + public void exportImportV2Zip() throws FileNotFoundException { // Prepare images BitmapDrawable launcher = (BitmapDrawable) ResourcesCompat.getDrawableForDensity(activity.getResources(), R.mipmap.ic_launcher, DisplayMetrics.DENSITY_XXXHIGH, activity.getTheme()); BitmapDrawable roundLauncher = (BitmapDrawable) ResourcesCompat.getDrawableForDensity(activity.getResources(), R.mipmap.ic_launcher_round, DisplayMetrics.DENSITY_XXXHIGH, activity.getTheme()); @@ -935,7 +913,7 @@ public class ImportExportTest // Export everything ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MultiFormatExporter.exportData(activity.getApplicationContext(), db, outputStream, DataFormat.Catima,null); + MultiFormatExporter.exportData(activity.getApplicationContext(), db, outputStream, DataFormat.Catima, null); // Wipe database TestHelpers.getEmptyDb(activity); @@ -959,25 +937,25 @@ public class ImportExportTest assertEquals(loyaltyCard.cardId, dbLoyaltyCard.cardId); assertEquals(loyaltyCard.barcodeId, dbLoyaltyCard.barcodeId); assertEquals(loyaltyCard.starStatus, dbLoyaltyCard.starStatus); - assertEquals(loyaltyCard.barcodeType != null ? loyaltyCard.barcodeType.format() : null, dbLoyaltyCard.barcodeType != null? dbLoyaltyCard.barcodeType.format() : null); + assertEquals(loyaltyCard.barcodeType != null ? loyaltyCard.barcodeType.format() : null, dbLoyaltyCard.barcodeType != null ? dbLoyaltyCard.barcodeType.format() : null); assertEquals(loyaltyCard.balanceType, dbLoyaltyCard.balanceType); assertEquals(loyaltyCard.headerColor, dbLoyaltyCard.headerColor); List emptyGroup = new ArrayList<>(); assertEquals( - groupsToGroupNames( - (List) Utils.mapGetOrDefault( - loyaltyCardGroups, - loyaltyCardID, - emptyGroup + groupsToGroupNames( + (List) Utils.mapGetOrDefault( + loyaltyCardGroups, + loyaltyCardID, + emptyGroup + ) + ), + groupsToGroupNames( + db.getLoyaltyCardGroups( + loyaltyCardID + ) ) - ), - groupsToGroupNames( - db.getLoyaltyCardGroups( - loyaltyCardID - ) - ) ); Bitmap expectedFrontImage = loyaltyCardFrontImages.get(loyaltyCardID); @@ -1000,8 +978,7 @@ public class ImportExportTest } @Test - public void importV2CSV() - { + public void importV2CSV() { String csvText = "2\n" + "\n" + "_id\n" +