From 3fd45af7d95ccdcfced410f96386f1330b884031 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:41:38 -0400 Subject: [PATCH 1/8] Add dependency on Guava --- app/build.gradle | 1 + app/src/main/java/protect/card_locker/MainActivity.java | 1 + 2 files changed, 2 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 32c9123ac..a01de9d38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,7 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.2.0@aar' compile 'com.google.zxing:core:3.2.1' compile 'org.apache.commons:commons-csv:1.2' + compile group: 'com.google.guava', name: 'guava', version: '18.0' testCompile 'junit:junit:4.12' testCompile "org.robolectric:robolectric:3.0" } diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 72ad9e9c4..42deefbd0 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -123,6 +123,7 @@ public class MainActivity extends AppCompatActivity final String[][] USED_LIBRARIES = new String[][] { new String[] {"Commons CSV", "https://commons.apache.org/proper/commons-csv/"}, + new String[] {"Guava", "https://github.com/google/guava"}, new String[] {"ZXing", "https://github.com/zxing/zxing"}, new String[] {"ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded"}, }; From 1c8ef34b8a2d5c2c7567aded75ff93597d8795e4 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:43:54 -0400 Subject: [PATCH 2/8] Remove unneeded parentActivtyName attributes --- app/src/main/AndroidManifest.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f55a3bfed..9a900e199 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,14 +36,12 @@ android:name=".LoyaltyCardViewActivity" android:theme="@style/AppTheme.NoActionBar" android:configChanges="orientation|screenSize" - android:windowSoftInputMode="stateHidden" - android:parentActivityName="protect.card_locker.MainActivity"/> + android:windowSoftInputMode="stateHidden"/> + android:theme="@style/AppTheme.NoActionBar"/> From 0aa18042583b8cb8db52f82a3df2826c72b8e6c2 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:44:40 -0400 Subject: [PATCH 3/8] Allow home button to back out to previous activity --- .../java/protect/card_locker/LoyaltyCardViewActivity.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index a11840d91..9d0dd90fa 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -267,6 +267,10 @@ public class LoyaltyCardViewActivity extends AppCompatActivity switch(id) { + case android.R.id.home: + finish(); + break; + case R.id.action_delete: Log.e(TAG, "Deleting card: " + loyaltyCardId); From 2f86de4c1b4f90e3621e7df0dc67859e05085800 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:50:06 -0400 Subject: [PATCH 4/8] Protect against unexpected failures when encoding barcodes It was observed that some barcode encoders will fail if the data passed to them is not valid for the format. For example, the ITF encoder will throw an ArrayIndexOutOfBoundsException on the input "this is a test". --- .../card_locker/BarcodeImageWriterTask.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java index d71bee2ac..a456f7239 100644 --- a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java +++ b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java @@ -45,7 +45,17 @@ class BarcodeImageWriterTask extends AsyncTask BitMatrix bitMatrix; try { - bitMatrix = writer.encode(cardId, format, imageWidth, imageHeight, null); + try + { + bitMatrix = writer.encode(cardId, format, imageWidth, imageHeight, null); + } + catch(Exception e) + { + // Cast a wider net here and catch any exception, as there are some + // cases where an encoder may fail if the data is invalid for the + // barcode type. If this happens, we want to fail gracefully. + throw new WriterException(e); + } final int WHITE = 0xFFFFFFFF; final int BLACK = 0xFF000000; @@ -86,9 +96,9 @@ class BarcodeImageWriterTask extends AsyncTask return bitmap; } - catch (WriterException | IllegalArgumentException e) + catch (WriterException e) { - Log.e(TAG, "Failed to generate barcode", e); + Log.e(TAG, "Failed to generate barcode of type " + format + ": " + cardId, e); } return null; From 793247a48c3a15467a4305b2468c34c653388216 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:50:21 -0400 Subject: [PATCH 5/8] Remove unused context --- app/src/main/res/layout/loyalty_card_view_activity.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/res/layout/loyalty_card_view_activity.xml b/app/src/main/res/layout/loyalty_card_view_activity.xml index 03aedbadb..13cb6113c 100644 --- a/app/src/main/res/layout/loyalty_card_view_activity.xml +++ b/app/src/main/res/layout/loyalty_card_view_activity.xml @@ -1,11 +1,9 @@ + android:fitsSystemWindows="true"> Date: Sat, 21 May 2016 22:50:57 -0400 Subject: [PATCH 6/8] Remove GlobalLayoutListener when no longer needed --- .../protect/card_locker/LoyaltyCardViewActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 9d0dd90fa..621f63c9a 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -2,6 +2,7 @@ package protect.card_locker; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v7.app.ActionBar; @@ -161,6 +162,15 @@ public class LoyaltyCardViewActivity extends AppCompatActivity @Override public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < 16) + { + barcodeImage.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + else + { + barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + Log.d(TAG, "ImageView size now known"); new BarcodeImageWriterTask(barcodeImage, cardIdString, format).execute(); } From 8edc9ce5fd4c79016ad1196247d7a73a279effc0 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Sat, 21 May 2016 22:52:57 -0400 Subject: [PATCH 7/8] Set barcode image visibility upon completion If the barcode generation succeeds set it as visible, otherwise make it gone. --- .../protect/card_locker/BarcodeImageWriterTask.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java index a456f7239..b7e5642e9 100644 --- a/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java +++ b/app/src/main/java/protect/card_locker/BarcodeImageWriterTask.java @@ -3,6 +3,7 @@ package protect.card_locker; import android.graphics.Bitmap; import android.os.AsyncTask; import android.util.Log; +import android.view.View; import android.widget.ImageView; import com.google.zxing.BarcodeFormat; @@ -106,6 +107,7 @@ class BarcodeImageWriterTask extends AsyncTask protected void onPostExecute(Bitmap result) { + Log.i(TAG, "Finished generating barcode image of type " + format + ": " + cardId); ImageView imageView = imageViewReference.get(); if(imageView == null) { @@ -114,5 +116,16 @@ class BarcodeImageWriterTask extends AsyncTask } imageView.setImageBitmap(result); + + if(result != null) + { + Log.i(TAG, "Displaying barcode"); + imageView.setVisibility(View.VISIBLE); + } + else + { + Log.i(TAG, "Barcode generation failed, removing image from display"); + imageView.setVisibility(View.GONE); + } } } \ No newline at end of file From c86819fc7447f61d582daa20c96fb65d85d0a409 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Mon, 23 May 2016 08:43:52 -0400 Subject: [PATCH 8/8] Allow user to enter a barcode manually If a user is unable to scan a barcode, this commit allows a user to enter is manually. If the user selects to Enter Card instead of Capture Card, the user may enter the card's id. As it may not be known which barcode format the user expects, and the user may not know what barcode type is what, all barcode types are generated from the user input. Those that are valid are displayed to the user. The user may then select the barcode image which matches what the user wants. Italian translations provided by Michael Moroni (Airon90) Dutch translations provided by PanderMusubi --- app/src/main/AndroidManifest.xml | 6 + .../card_locker/BarcodeSelectorActivity.java | 202 +++++++++++++++++ .../card_locker/LoyaltyCardViewActivity.java | 74 ++++--- .../res/layout/barcode_selector_activity.xml | 205 ++++++++++++++++++ .../res/layout/loyalty_card_view_activity.xml | 5 + app/src/main/res/values-it/strings.xml | 3 + app/src/main/res/values-nl/strings.xml | 3 + app/src/main/res/values/strings.xml | 4 + 8 files changed, 468 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java create mode 100644 app/src/main/res/layout/barcode_selector_activity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9a900e199..59b46dad5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,12 @@ android:theme="@style/AppTheme.NoActionBar" android:configChanges="orientation|screenSize" android:windowSoftInputMode="stateHidden"/> + SUPPORTED_BARCODE_TYPES = Collections.unmodifiableList( + Arrays.asList( + BarcodeFormat.AZTEC.name(), + BarcodeFormat.CODE_39.name(), + BarcodeFormat.CODE_128.name(), + BarcodeFormat.CODABAR.name(), + BarcodeFormat.DATA_MATRIX.name(), + BarcodeFormat.EAN_8.name(), + BarcodeFormat.EAN_13.name(), + BarcodeFormat.ITF.name(), + BarcodeFormat.PDF_417.name(), + BarcodeFormat.QR_CODE.name(), + BarcodeFormat.UPC_A.name() + )); + + private Map barcodeViewMap; + private LinkedList barcodeGeneratorTasks = new LinkedList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.barcode_selector_activity); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if(actionBar != null) + { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + barcodeViewMap = ImmutableMap.builder() + .put(BarcodeFormat.AZTEC.name(), R.id.aztecBarcode) + .put(BarcodeFormat.CODE_39.name(), R.id.code39Barcode) + .put(BarcodeFormat.CODE_128.name(), R.id.code128Barcode) + .put(BarcodeFormat.CODABAR.name(), R.id.codabarBarcode) + .put(BarcodeFormat.DATA_MATRIX.name(), R.id.datamatrixBarcode) + .put(BarcodeFormat.EAN_8.name(), R.id.ean8Barcode) + .put(BarcodeFormat.EAN_13.name(), R.id.ean13Barcode) + .put(BarcodeFormat.ITF.name(), R.id.itfBarcode) + .put(BarcodeFormat.PDF_417.name(), R.id.pdf417Barcode) + .put(BarcodeFormat.QR_CODE.name(), R.id.qrcodeBarcode) + .put(BarcodeFormat.UPC_A.name(), R.id.upcaBarcode) + .build(); + + EditText cardId = (EditText) findViewById(R.id.cardId); + cardId.addTextChangedListener(new TextWatcher() + { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { + // Noting to do + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) + { + Log.d(TAG, "Entered text: " + s); + + // Stop any async tasks which may not have been started yet + for(AsyncTask task : barcodeGeneratorTasks) + { + task.cancel(false); + } + barcodeGeneratorTasks.clear(); + + // Update barcodes + for(String key : barcodeViewMap.keySet()) + { + ImageView image = (ImageView)findViewById(barcodeViewMap.get(key)); + createBarcodeOption(image, key, s.toString()); + } + } + + @Override + public void afterTextChanged(Editable s) + { + // Noting to do + } + }); + } + + private void createBarcodeOption(final ImageView image, final String formatType, final String cardId) + { + final BarcodeFormat format = BarcodeFormat.valueOf(formatType); + if(format == null) + { + Log.w(TAG, "Unsupported barcode format: " + formatType); + return; + } + + image.setImageBitmap(null); + image.setVisibility(View.GONE); + image.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + Log.d(TAG, "Selected barcode type " + formatType); + Intent result = new Intent(); + result.putExtra(BARCODE_FORMAT, formatType); + result.putExtra(BARCODE_CONTENTS, cardId); + BarcodeSelectorActivity.this.setResult(RESULT_OK, result); + finish(); + } + }); + + 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()); + if (Build.VERSION.SDK_INT < 16) + { + image.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + else + { + image.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + Log.d(TAG, "Generating barcode for type " + formatType); + BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format); + barcodeGeneratorTasks.add(task); + task.execute(); + } + }); + } + else + { + Log.d(TAG, "Generating barcode for type " + formatType); + BarcodeImageWriterTask task = new BarcodeImageWriterTask(image, cardId, format); + barcodeGeneratorTasks.add(task); + task.execute(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == android.R.id.home) + { + setResult(Activity.RESULT_CANCELED); + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 621f63c9a..3b8f72a9c 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -1,6 +1,7 @@ package protect.card_locker; +import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; @@ -21,31 +22,12 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - public class LoyaltyCardViewActivity extends AppCompatActivity { private static final String TAG = "CardLocker"; - // These are all the barcode types that the zxing library - // is able to generate a barcode for, and thus should be - // the only barcodes which we should attempt to scan. - Collection supportedBarcodeTypes = Collections.unmodifiableList(Arrays.asList( - BarcodeFormat.AZTEC.name(), - BarcodeFormat.CODE_39.name(), - BarcodeFormat.CODE_128.name(), - BarcodeFormat.CODABAR.name(), - BarcodeFormat.DATA_MATRIX.name(), - BarcodeFormat.EAN_8.name(), - BarcodeFormat.EAN_13.name(), - BarcodeFormat.ITF.name(), - BarcodeFormat.PDF_417.name(), - BarcodeFormat.QR_CODE.name(), - BarcodeFormat.UPC_A.name() - )); + private static final int SELECT_BARCODE_REQUEST = 1; @Override protected void onCreate(Bundle savedInstanceState) @@ -85,6 +67,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity final View barcodeCaptureLayout = findViewById(R.id.barcodeCaptureLayout); final Button captureButton = (Button) findViewById(R.id.captureButton); + final Button enterButton = (Button) findViewById(R.id.enterButton); final Button saveButton = (Button) findViewById(R.id.saveButton); final Button cancelButton = (Button) findViewById(R.id.cancelButton); @@ -192,7 +175,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity public void onClick(View v) { IntentIntegrator integrator = new IntentIntegrator(LoyaltyCardViewActivity.this); - integrator.setDesiredBarcodeFormats(supportedBarcodeTypes); + integrator.setDesiredBarcodeFormats(BarcodeSelectorActivity.SUPPORTED_BARCODE_TYPES); String prompt = getResources().getString(R.string.scanCardBarcode); integrator.setPrompt(prompt); @@ -202,6 +185,16 @@ public class LoyaltyCardViewActivity extends AppCompatActivity captureButton.setOnClickListener(captureCallback); + enterButton.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class); + startActivityForResult(i, SELECT_BARCODE_REQUEST); + } + }); + saveButton.setOnClickListener(new View.OnClickListener() { @Override @@ -305,24 +298,37 @@ public class LoyaltyCardViewActivity extends AppCompatActivity @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { + String contents = null; + String format = null; + IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (result != null) { - String contents = result.getContents(); - String format = result.getFormatName(); - if(contents != null && contents.isEmpty() == false && - format != null && format.isEmpty() == false) - { - Log.i(TAG, "Read Contents from scan: " + contents); - Log.i(TAG, "Read Format: " + format); + Log.i(TAG, "Received barcode information from capture"); + contents = result.getContents(); + format = result.getFormatName(); + } - final EditText cardIdField = (EditText) findViewById(R.id.cardId); - cardIdField.setText(contents); - final EditText barcodeTypeField = (EditText) findViewById(R.id.barcodeType); - barcodeTypeField.setText(format); - onResume(); - } + if(requestCode == SELECT_BARCODE_REQUEST && resultCode == Activity.RESULT_OK) + { + Log.i(TAG, "Received barcode information from capture"); + + contents = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_CONTENTS); + format = intent.getStringExtra(BarcodeSelectorActivity.BARCODE_FORMAT); + } + + if(contents != null && contents.isEmpty() == false && + format != null && format.isEmpty() == false) + { + Log.i(TAG, "Read barcode id: " + contents); + Log.i(TAG, "Read format: " + format); + + final EditText cardIdField = (EditText) findViewById(R.id.cardId); + cardIdField.setText(contents); + final EditText barcodeTypeField = (EditText) findViewById(R.id.barcodeType); + barcodeTypeField.setText(format); + onResume(); } } } \ No newline at end of file diff --git a/app/src/main/res/layout/barcode_selector_activity.xml b/app/src/main/res/layout/barcode_selector_activity.xml new file mode 100644 index 000000000..e5884af2e --- /dev/null +++ b/app/src/main/res/layout/barcode_selector_activity.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/loyalty_card_view_activity.xml b/app/src/main/res/layout/loyalty_card_view_activity.xml index 13cb6113c..c42b02138 100644 --- a/app/src/main/res/layout/loyalty_card_view_activity.xml +++ b/app/src/main/res/layout/loyalty_card_view_activity.xml @@ -136,6 +136,11 @@ android:layout_height="wrap_content" android:text="@string/capture" android:layout_weight="1.0"/> +