diff --git a/CHANGELOG.md b/CHANGELOG.md index e465fae52..170a8c40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Changes: Changes: +- Support adding a front and back photo to each card - Support new PDF417 export from Voucher Vault - Support copying multiple barcodes at once - Support sharing multiple loyalty cards at once diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index bf7ad087a..788f3c593 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -6,9 +6,11 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; +import android.graphics.Bitmap; import com.google.zxing.BarcodeFormat; +import java.io.FileNotFoundException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Currency; @@ -52,9 +54,13 @@ public class DBHelper extends SQLiteOpenHelper public static final String groupID = "groupId"; } + private Context mContext; + public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); + + mContext = context; } @Override @@ -78,7 +84,7 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbIds.CARD_ID + " TEXT not null," + LoyaltyCardDbIds.BARCODE_ID + " TEXT," + LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," + - LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0' )"); + LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0')"); // create associative table for cards in groups db.execSQL("create table " + LoyaltyCardDbIdsGroups.TABLE + "(" + @@ -426,7 +432,7 @@ public class DBHelper extends SQLiteOpenHelper } } - public boolean deleteLoyaltyCard (final int id) + public boolean deleteLoyaltyCard(final int id) { SQLiteDatabase db = getWritableDatabase(); // Delete card @@ -439,6 +445,14 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbIdsGroups.cardID + " = ? ", new String[]{String.format("%d", id)}); + // Also wipe card images associated with this card + try { + Utils.saveCardImage(mContext, null, id, true); + Utils.saveCardImage(mContext, null, id, false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return (rowsDeleted == 1); } diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index 8e288b345..96ad7bf39 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -2,6 +2,7 @@ package protect.card_locker; import android.app.Activity; import android.app.ProgressDialog; +import android.content.Context; import android.content.DialogInterface; import android.os.AsyncTask; import android.util.Log; @@ -56,26 +57,26 @@ class ImportExportTask extends AsyncTask this.listener = listener; } - private boolean performImport(InputStream stream, DBHelper db) + private boolean performImport(Context context, InputStream stream, DBHelper db) { boolean result = false; - result = MultiFormatImporter.importData(db, stream, format); + result = MultiFormatImporter.importData(context, db, stream, format); Log.i(TAG, "Import result: " + result); return result; } - private boolean performExport(OutputStream stream, DBHelper db) + private boolean performExport(Context context, OutputStream stream, DBHelper db) { boolean result = false; try { OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8")); - result = MultiFormatExporter.exportData(db, writer, format); + result = MultiFormatExporter.exportData(context, db, writer, format); writer.close(); } catch (IOException e) @@ -112,11 +113,11 @@ class ImportExportTask extends AsyncTask if(doImport) { - result = performImport(inputStream, db); + result = performImport(activity.getApplicationContext(), inputStream, db); } else { - result = performExport(outputStream, db); + result = performExport(activity.getApplicationContext(), outputStream, db); } return result; diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index 3d2f8e481..49bf0d39f 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -2,6 +2,7 @@ package protect.card_locker; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.net.Uri; import com.google.zxing.BarcodeFormat; @@ -21,7 +22,6 @@ public class ImportURIHelper { private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID; private static final String BARCODE_ID = DBHelper.LoyaltyCardDbIds.BARCODE_ID; private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE; - private static final String HEADER_COLOR = DBHelper.LoyaltyCardDbIds.HEADER_COLOR; private final Context context; @@ -125,7 +125,8 @@ public class ImportURIHelper { if(loyaltyCard.headerColor != null) { uriBuilder.appendQueryParameter(HEADER_COLOR, loyaltyCard.headerColor.toString()); } - //StarStatus will not be exported + // Star status will not be exported + // Front and back pictures are often too big to fit into a message in base64 nicely, not sharing either... return uriBuilder.build(); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index e2fc98473..91445355d 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -1,6 +1,10 @@ package protect.card_locker; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.sql.Blob; import com.google.zxing.BarcodeFormat; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 4cb82e120..f24119ddb 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -1,16 +1,23 @@ package protect.card_locker; +import android.Manifest; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.LocaleList; +import android.provider.MediaStore; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -38,6 +45,9 @@ import com.google.zxing.BarcodeFormat; import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InvalidObjectException; import java.math.BigDecimal; import java.text.DateFormat; @@ -47,21 +57,33 @@ import java.util.Collections; import java.util.Currency; import java.util.Date; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; import androidx.fragment.app.DialogFragment; public class LoyaltyCardEditActivity extends AppCompatActivity { private static final String TAG = "Catima"; + private final String STATE_TAB_INDEX = "savedTab"; + + private static final int ID_IMAGE_FRONT = 0; + private static final int ID_IMAGE_BACK = 1; + + private static final int PERMISSION_REQUEST_CAMERA_IMAGE_FRONT = 100; + private static final int PERMISSION_REQUEST_CAMERA_IMAGE_BACK = 101; + TabLayout tabs; ImageView thumbnail; @@ -77,6 +99,13 @@ public class LoyaltyCardEditActivity extends AppCompatActivity ImageView barcodeImage; View barcodeImageLayout; View barcodeCaptureLayout; + View cardImageFrontHolder; + View cardImageBackHolder; + ImageView cardImageFront; + ImageView cardImageBack; + + Bitmap frontImageBitmap; + Bitmap backImageBitmap; Button enterButton; @@ -102,6 +131,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity HashMap currencies = new HashMap<>(); + String tempCameraPicturePath; + private void extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); @@ -169,6 +200,12 @@ public class LoyaltyCardEditActivity extends AppCompatActivity barcodeImage = findViewById(R.id.barcode); barcodeImageLayout = findViewById(R.id.barcodeLayout); barcodeCaptureLayout = findViewById(R.id.barcodeCaptureLayout); + cardImageFrontHolder = findViewById(R.id.frontImageHolder); + cardImageBackHolder = findViewById(R.id.backImageHolder); + cardImageFrontHolder.setId(ID_IMAGE_FRONT); + cardImageBackHolder.setId(ID_IMAGE_BACK); + cardImageFront = findViewById(R.id.frontImage); + cardImageBack = findViewById(R.id.backImage); enterButton = findViewById(R.id.enterButton); @@ -489,6 +526,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity barcodeIdField.setTag(null); barcodeIdField.setText(""); barcodeTypeField.setText(""); + cardImageFront.setTag(null); + cardImageBack.setTag(null); } @SuppressLint("DefaultLocale") @@ -563,6 +602,16 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } } + if(cardImageFront.getTag() == null) + { + setCardImage(cardImageFront, Utils.retrieveCardImage(this, loyaltyCard.id, true)); + } + + if(cardImageBack.getTag() == null) + { + setCardImage(cardImageBack, Utils.retrieveCardImage(this, loyaltyCard.id, false)); + } + setTitle(R.string.editCardTitle); } else if(importLoyaltyCardUri != null) @@ -601,6 +650,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity balanceCurrencyField.setTag(null); formatBalanceCurrencyField(null); hideBarcode(); + setCardImage(cardImageFront, null); + setCardImage(cardImageBack, null); } if(groupsChips.getChildCount() == 0) @@ -679,12 +730,25 @@ public class LoyaltyCardEditActivity extends AppCompatActivity enterButton.setOnClickListener(new EditCardIdAndBarcode()); barcodeImage.setOnClickListener(new EditCardIdAndBarcode()); + cardImageFrontHolder.setOnClickListener(new ChooseCardImage()); + cardImageBackHolder.setOnClickListener(new ChooseCardImage()); + FloatingActionButton saveButton = findViewById(R.id.fabSave); saveButton.setOnClickListener(v -> doSave()); generateIcon(storeFieldEdit.getText().toString()); } + private void setCardImage(ImageView imageView, Bitmap bitmap) { + imageView.setTag(bitmap); + + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + imageView.setImageResource(R.drawable.ic_camera_white); + } + } + private void formatExpiryField(Date expiry) { if (expiry == null) { expiryField.setText(getString(R.string.never)); @@ -706,6 +770,23 @@ public class LoyaltyCardEditActivity extends AppCompatActivity askBeforeQuitIfChanged(); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + try { + if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_FRONT) { + takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_FRONT); + } else { + takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_BACK); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + private void askBarcodeChange(Runnable callback) { if (tempStoredOldBarcodeValue.equals(cardIdFieldView.getText().toString())) { // They are the same, don't ask @@ -782,12 +863,29 @@ public class LoyaltyCardEditActivity extends AppCompatActivity confirmExitDialog.show(); } + private void takePhotoForCard(int type) throws IOException { + Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + + String imageFileName = "CATIMA_" + new Date().getTime(); + File image = File.createTempFile( + imageFileName, + ".jpg", + getExternalFilesDir(Environment.DIRECTORY_PICTURES) + ); + + tempCameraPicturePath = image.getAbsolutePath(); + + Uri photoURI = FileProvider.getUriForFile(LoyaltyCardEditActivity.this, BuildConfig.APPLICATION_ID, image); + i.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + + startActivityForResult(i, type); + } + class EditCardIdAndBarcode implements View.OnClickListener { @Override public void onClick(View v) { - Intent i = new Intent(getApplicationContext(), ScanActivity.class); final Bundle b = new Bundle(); b.putString("cardId", cardIdFieldView.getText().toString()); @@ -796,6 +894,56 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } } + class ChooseCardImage implements View.OnClickListener + { + @Override + public void onClick(View v) throws NoSuchElementException + { + ImageView targetView = v.getId() == ID_IMAGE_FRONT ? cardImageFront : cardImageBack; + + LinkedHashMap> cardOptions = new LinkedHashMap<>(); + if (targetView.getTag() != null) { + cardOptions.put(getString(R.string.removeImage), () -> { + setCardImage(targetView, null); + return null; + }); + } + + cardOptions.put(getString(R.string.takePhoto), () -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(new String[]{Manifest.permission.CAMERA}, v.getId() == ID_IMAGE_FRONT ? PERMISSION_REQUEST_CAMERA_IMAGE_FRONT : PERMISSION_REQUEST_CAMERA_IMAGE_BACK); + } else { + takePhotoForCard(v.getId() == ID_IMAGE_FRONT ? Utils.CARD_IMAGE_FROM_CAMERA_FRONT : Utils.CARD_IMAGE_FROM_CAMERA_BACK); + } + return null; + }); + + cardOptions.put(getString(R.string.chooseImageFromGallery), () -> { + Intent i = new Intent(Intent.ACTION_PICK); + i.setType("image/*"); + startActivityForResult(i, v.getId() == ID_IMAGE_FRONT ? Utils.CARD_IMAGE_FROM_FILE_FRONT : Utils.CARD_IMAGE_FROM_FILE_BACK); + return null; + }); + + new AlertDialog.Builder(LoyaltyCardEditActivity.this) + .setTitle(v.getId() == ID_IMAGE_FRONT ? getString(R.string.setFrontImage) : getString(R.string.setBackImage)) + .setItems(cardOptions.keySet().toArray(new CharSequence[cardOptions.size()]), (dialog, which) -> { + Iterator> callables = cardOptions.values().iterator(); + Callable callable = callables.next(); + + for (int i = 0; i < which; i++) { + callable = callables.next(); + } + + try { + callable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + }) + .show(); + } + } class ColorSelectListener implements View.OnClickListener { @@ -878,8 +1026,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } } - private void doSave() - { + private void doSave() { if (tempStoredOldBarcodeValue != null) { askBarcodeChange(this::doSave); return; @@ -922,11 +1069,23 @@ public class LoyaltyCardEditActivity extends AppCompatActivity if(updateLoyaltyCard) { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) db.updateLoyaltyCard(loyaltyCardId, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headingColorValue); + try { + Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); + Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } Log.i(TAG, "Updated " + loyaltyCardId + " to " + cardId); } else { loyaltyCardId = (int)db.insertLoyaltyCard(store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headingColorValue, 0); + try { + Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); + Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } } db.setLoyaltyCardGroups(loyaltyCardId, selectedGroups); @@ -1002,12 +1161,57 @@ public class LoyaltyCardEditActivity extends AppCompatActivity { super.onActivityResult(requestCode, resultCode, intent); - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); + if (requestCode == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || requestCode == Utils.CARD_IMAGE_FROM_CAMERA_BACK) { + if (resultCode == RESULT_OK) { + Bitmap bitmap = BitmapFactory.decodeFile(tempCameraPicturePath); - if (resultCode == RESULT_OK) { - cardId = barcodeValues.content(); - barcodeType = barcodeValues.format(); - barcodeId = ""; + if (bitmap != null) { + bitmap = Utils.resizeBitmap(bitmap); + try { + bitmap = Utils.rotateBitmap(bitmap, new ExifInterface(tempCameraPicturePath)); + } catch (IOException e) { + e.printStackTrace(); + } + + if (requestCode == Utils.CARD_IMAGE_FROM_CAMERA_FRONT) { + setCardImage(cardImageFront, bitmap); + } else { + setCardImage(cardImageBack, bitmap); + } + + hasChanged = true; + } + } + } else if (requestCode == Utils.CARD_IMAGE_FROM_FILE_FRONT || requestCode == Utils.CARD_IMAGE_FROM_FILE_BACK) { + if (resultCode == RESULT_OK) { + Bitmap bitmap = null; + try { + bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), intent.getData()); + } catch (IOException e) { + Log.e(TAG, "Error getting data from image file"); + e.printStackTrace(); + Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); + } + + if (bitmap != null) { + bitmap = Utils.resizeBitmap(bitmap); + if (requestCode == Utils.CARD_IMAGE_FROM_FILE_FRONT) { + setCardImage(cardImageFront, bitmap); + } else { + setCardImage(cardImageBack, bitmap); + } + + hasChanged = true; + } + } + } else { + BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); + + if (resultCode == RESULT_OK) { + cardId = barcodeValues.content(); + barcodeType = barcodeValues.format(); + barcodeId = ""; + } } onResume(); @@ -1081,19 +1285,29 @@ public class LoyaltyCardEditActivity extends AppCompatActivity View cardPart = findViewById(R.id.cardPart); View barcodePart = findViewById(R.id.barcodePart); + View picturesPart = findViewById(R.id.picturesPart); if (getString(R.string.card).equals(part)) { cardPart.setVisibility(View.VISIBLE); barcodePart.setVisibility(View.GONE); + picturesPart.setVisibility(View.GONE); // Explicitly hide barcode (fixes blurriness on redraw) hideBarcode(); } else if (getString(R.string.barcode).equals(part)) { cardPart.setVisibility(View.GONE); barcodePart.setVisibility(View.VISIBLE); + picturesPart.setVisibility(View.GONE); // Redraw barcode due to size change (Visibility.GONE sets it to 0) generateOrHideBarcode(); + } else if (getString(R.string.photos).equals(part)) { + cardPart.setVisibility(View.GONE); + barcodePart.setVisibility(View.GONE); + picturesPart.setVisibility(View.VISIBLE); + + // Explicitly hide barcode (fixes blurriness on redraw) + hideBarcode(); } else { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 28eb59169..779e36d08 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -3,6 +3,7 @@ package protect.card_locker; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; @@ -18,6 +19,7 @@ import android.view.Window; import android.view.WindowManager; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; @@ -49,7 +51,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity TextView cardIdFieldView; BottomSheetBehavior behavior; View bottomSheet; + View bottomSheetContentWrapper; ImageView bottomSheetButton; + View frontImageView; + ImageView frontImage; + View backImageView; + ImageView backImage; TextView noteView; TextView groupsView; TextView balanceView; @@ -76,9 +83,19 @@ public class LoyaltyCardViewActivity extends AppCompatActivity Guideline centerGuideline; SeekBar barcodeScaler; + Bitmap frontImageBitmap; + Bitmap backImageBitmap; + boolean starred; boolean backgroundNeedsDarkIcons; - boolean barcodeIsFullscreen = false; + FullscreenType fullscreenType = FullscreenType.NONE; + + enum FullscreenType { + NONE, + BARCODE, + IMAGE_FRONT, + IMAGE_BACK + } private void extractIntentFields(Intent intent) { @@ -120,7 +137,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity cardIdFieldView = findViewById(R.id.cardIdView); bottomSheet = findViewById(R.id.bottom_sheet); + bottomSheetContentWrapper = findViewById(R.id.bottomSheetContentWrapper); bottomSheetButton = findViewById(R.id.bottomSheetButton); + frontImageView = findViewById(R.id.frontImageView); + frontImage = findViewById(R.id.frontImage); + backImageView = findViewById(R.id.backImageView); + backImage = findViewById(R.id.backImage); noteView = findViewById(R.id.noteView); groupsView = findViewById(R.id.groupsView); balanceView = findViewById(R.id.balanceView); @@ -144,7 +166,9 @@ public class LoyaltyCardViewActivity extends AppCompatActivity float scale = (float) progress / (float) barcodeScaler.getMax(); Log.d(TAG, "Scaling to " + scale); - redrawBarcodeAfterResize(); + if (fullscreenType == FullscreenType.BARCODE) { + redrawBarcodeAfterResize(); + } centerGuideline.setGuidelinePercent(0.5f * scale); } @@ -162,18 +186,29 @@ public class LoyaltyCardViewActivity extends AppCompatActivity rotationEnabled = true; // Allow making barcode fullscreen on tap - maximizeButton.setOnClickListener(v -> setFullscreen(true)); + maximizeButton.setOnClickListener(v -> setFullscreen(FullscreenType.BARCODE)); barcodeImage.setOnClickListener(view -> { - if (barcodeIsFullscreen) - { - setFullscreen(false); - } - else - { - setFullscreen(true); + if (fullscreenType != FullscreenType.NONE) { + setFullscreen(FullscreenType.NONE); + } else { + setFullscreen(FullscreenType.BARCODE); } }); - minimizeButton.setOnClickListener(v -> setFullscreen(false)); + frontImageView.setOnClickListener(view -> { + if (fullscreenType != FullscreenType.IMAGE_FRONT) { + setFullscreen(FullscreenType.IMAGE_FRONT); + } else { + setFullscreen(FullscreenType.NONE); + } + }); + backImageView.setOnClickListener(view -> { + if (fullscreenType != FullscreenType.IMAGE_BACK) { + setFullscreen(FullscreenType.IMAGE_BACK); + } else { + setFullscreen(FullscreenType.NONE); + } + }); + minimizeButton.setOnClickListener(v -> setFullscreen(FullscreenType.NONE)); editButton = findViewById(R.id.fabEdit); editButton.setOnClickListener(v -> { @@ -197,7 +232,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity editButton.hide(); } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) { bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24); - editButton.show(); + if (fullscreenType == FullscreenType.NONE) { + editButton.show(); + } + + // Scroll bottomsheet content back to top + bottomSheetContentWrapper.setScrollY(0); } } @@ -212,6 +252,25 @@ public class LoyaltyCardViewActivity extends AppCompatActivity behavior.setState(BottomSheetBehavior.STATE_EXPANDED); } }); + + // Fix bottom sheet content sizing + ViewTreeObserver viewTreeObserver = bottomSheet.getViewTreeObserver(); + viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + bottomSheet.getViewTreeObserver().removeOnGlobalLayoutListener(this); + DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + int height = displayMetrics.heightPixels; + int maxHeight = height - appBarLayout.getHeight() - bottomSheetButton.getHeight(); + Log.d(TAG, "Button sheet should be " + maxHeight + " pixels high"); + bottomSheetContentWrapper.setLayoutParams( + new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + maxHeight + ) + ); + } + }); } @Override @@ -275,6 +334,22 @@ public class LoyaltyCardViewActivity extends AppCompatActivity settings.getFontSizeMin(settings.getLargeFont()), settings.getFontSizeMax(settings.getLargeFont()), 1, TypedValue.COMPLEX_UNIT_SP); + frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, true); + if (frontImageBitmap != null) { + frontImageView.setVisibility(View.VISIBLE); + frontImage.setImageBitmap(frontImageBitmap); + } else { + frontImageView.setVisibility(View.GONE); + } + + backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, false); + if (backImageBitmap != null) { + backImageView.setVisibility(View.VISIBLE); + backImage.setImageBitmap(backImageBitmap); + } else { + backImageView.setVisibility(View.GONE); + } + if(loyaltyCard.note.length() > 0) { noteView.setVisibility(View.VISIBLE); @@ -331,7 +406,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity } expiryView.setTag(loyaltyCard.expiry); - if (!barcodeIsFullscreen) { + if (fullscreenType != FullscreenType.NONE) { makeBottomSheetVisibleIfUseful(); } @@ -394,7 +469,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity if(format != null && isBarcodeSupported) { - if (!barcodeIsFullscreen) { + if (fullscreenType == FullscreenType.NONE) { maximizeButton.setVisibility(View.VISIBLE); } barcodeImage.setVisibility(View.VISIBLE); @@ -419,7 +494,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity } // Force redraw fullscreen state - setFullscreen(barcodeIsFullscreen); + setFullscreen(fullscreenType); } else { @@ -430,9 +505,9 @@ public class LoyaltyCardViewActivity extends AppCompatActivity @Override public void onBackPressed() { - if (barcodeIsFullscreen) + if (fullscreenType != FullscreenType.NONE) { - setFullscreen(false); + setFullscreen(FullscreenType.NONE); return; } @@ -566,7 +641,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity private void makeBottomSheetVisibleIfUseful() { - if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) { + if (frontImageView.getVisibility() == View.VISIBLE || backImageView.getVisibility() == View.VISIBLE || noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) { bottomSheet.setVisibility(View.VISIBLE); } else @@ -604,14 +679,21 @@ public class LoyaltyCardViewActivity extends AppCompatActivity * The purpose of this function is to make sure the barcode can be scanned from the phone * by machines which offer no space to insert the complete device. */ - private void setFullscreen(boolean enable) + private void setFullscreen(FullscreenType fullscreenType) { ActionBar actionBar = getSupportActionBar(); - if(enable) + if (fullscreenType != FullscreenType.NONE) { Log.d(TAG, "Move into of fullscreen"); - // Prepare redraw after size change - redrawBarcodeAfterResize(); + + if (fullscreenType == FullscreenType.IMAGE_FRONT) { + barcodeImage.setImageBitmap(frontImageBitmap); + } else if (fullscreenType == FullscreenType.IMAGE_BACK) { + barcodeImage.setImageBitmap(backImageBitmap); + } else { + // Prepare redraw after size change + redrawBarcodeAfterResize(); + } // Hide maximize and show minimize button and scaler maximizeButton.setVisibility(View.GONE); @@ -646,11 +728,8 @@ public class LoyaltyCardViewActivity extends AppCompatActivity | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN ); - - // Set current state - barcodeIsFullscreen = true; } - else if(!enable) + else { Log.d(TAG, "Move out of fullscreen"); @@ -690,9 +769,8 @@ public class LoyaltyCardViewActivity extends AppCompatActivity & ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY & ~View.SYSTEM_UI_FLAG_FULLSCREEN ); - - // Set current state - barcodeIsFullscreen = false; } + + this.fullscreenType = fullscreenType; } } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 23690cd00..35b00cd73 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -4,8 +4,12 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Matrix; +import android.media.ExifInterface; import android.provider.MediaStore; +import android.util.Base64; import android.util.Log; import android.widget.Toast; @@ -17,6 +21,10 @@ import com.google.zxing.RGBLuminanceSource; import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.math.BigDecimal; import java.text.NumberFormat; @@ -35,9 +43,15 @@ public class Utils { public static final int SELECT_BARCODE_REQUEST = 2; public static final int BARCODE_SCAN = 3; public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4; + public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 5; + public static final int CARD_IMAGE_FROM_CAMERA_BACK = 6; + public static final int CARD_IMAGE_FROM_FILE_FRONT = 7; + public static final int CARD_IMAGE_FROM_FILE_BACK = 8; static final double LUMINANCE_MIDPOINT = 0.5; + static final int BITMAP_SIZE_BIG = 512; + static public LetterBitmap generateIcon(Context context, String store, Integer backgroundColor) { return generateIcon(context, store, backgroundColor, false); } @@ -208,4 +222,127 @@ public class Utils { // Parse as BigDecimal return new BigDecimal(value); } + + static public byte[] bitmapToByteArray(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos); + return bos.toByteArray(); + } + + static public Bitmap byteArrayToBitmap(byte[] byteArray) { + if (byteArray == null) { + return null; + } + + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + } + + static public String bitmapToBase64(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + + return Base64.encodeToString(bitmapToByteArray(bitmap), Base64.URL_SAFE); + } + + static public Bitmap base64ToBitmap(String base64) { + if (base64 == null) { + return null; + } + + return byteArrayToBitmap(Base64.decode(base64, Base64.URL_SAFE)); + } + + static public Bitmap resizeBitmap(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + + Integer maxSize = BITMAP_SIZE_BIG; + + Integer width = bitmap.getWidth(); + Integer height = bitmap.getHeight(); + + if (height > width) { + Integer scale = height / maxSize; + height = maxSize; + width = width / scale; + } else if (width > height) { + Integer scale = width / maxSize; + width = maxSize; + height = height / scale; + } else { + height = maxSize; + width = maxSize; + } + + return Bitmap.createScaledBitmap(bitmap, width, height, true); + } + + static public Bitmap rotateBitmap(Bitmap bitmap, ExifInterface exifInterface) { + switch (exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) { + case ExifInterface.ORIENTATION_ROTATE_90: + return rotateBitmap(bitmap, 90f); + case ExifInterface.ORIENTATION_ROTATE_180: + return rotateBitmap(bitmap, 180f); + case ExifInterface.ORIENTATION_ROTATE_270: + return rotateBitmap(bitmap, 270f); + default: + return bitmap; + } + } + + static public Bitmap rotateBitmap(Bitmap bitmap, float rotation) { + if (rotation == 0) { + return bitmap; + } + + Matrix matrix = new Matrix(); + matrix.postRotate(rotation); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + static private String getCardImageFileName(int loyaltyCardId, boolean front) { + StringBuilder cardImageFileNameBuilder = new StringBuilder(); + + cardImageFileNameBuilder.append("card_"); + cardImageFileNameBuilder.append(loyaltyCardId); + cardImageFileNameBuilder.append("_"); + if (front) { + cardImageFileNameBuilder.append("front"); + } else { + cardImageFileNameBuilder.append("back"); + } + cardImageFileNameBuilder.append(".png"); + + return cardImageFileNameBuilder.toString(); + } + + static public void saveCardImage(Context context, Bitmap bitmap, int loyaltyCardId, boolean front) throws FileNotFoundException { + String fileName = getCardImageFileName(loyaltyCardId, front); + + if (bitmap == null) { + context.deleteFile(fileName); + return; + } + + FileOutputStream out = context.openFileOutput(getCardImageFileName(loyaltyCardId, front), Context.MODE_PRIVATE); + + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + } + + static public Bitmap retrieveCardImage(Context context, int loyaltyCardId, boolean front) { + FileInputStream in; + try { + in = context.openFileInput(getCardImageFileName(loyaltyCardId, front)); + } catch (FileNotFoundException e) { + return null; + } + + return BitmapFactory.decodeStream(in); + } } diff --git a/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java index 74dd8f94e..b8b96ed82 100644 --- a/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java +++ b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java @@ -1,10 +1,38 @@ package protect.card_locker.importexport; +import android.graphics.Bitmap; + import org.apache.commons.csv.CSVRecord; import protect.card_locker.FormatException; +import protect.card_locker.Utils; public class CSVHelpers { + static String IMAGE_FRONT = "frontimage"; + static String IMAGE_BACK = "backimage"; + + /** + * Extract an image from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, defaultValue is returned + * if it is not null. Otherwise, a FormatException is thrown. + */ + static Bitmap extractImage(String key, CSVRecord record) + { + if(record.isMapped(key)) + { + String value = record.get(key); + + if (value.isEmpty()) { + return null; + } + + return Utils.base64ToBitmap(value); + } + + return null; + } + /** * Extract a string from the items array. The index into the array * is determined by looking up the index in the fields map using the diff --git a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java b/app/src/main/java/protect/card_locker/importexport/CsvExporter.java similarity index 84% rename from app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java rename to app/src/main/java/protect/card_locker/importexport/CsvExporter.java index 03adaec8c..f5859b867 100644 --- a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvExporter.java @@ -1,5 +1,6 @@ package protect.card_locker.importexport; +import android.content.Context; import android.database.Cursor; import org.apache.commons.csv.CSVFormat; @@ -11,14 +12,15 @@ import java.io.OutputStreamWriter; import protect.card_locker.DBHelper; import protect.card_locker.Group; import protect.card_locker.LoyaltyCard; +import protect.card_locker.Utils; /** * Class for exporting the database into CSV (Comma Separate Values) * format. */ -public class CsvDatabaseExporter implements DatabaseExporter +public class CsvExporter implements Exporter { - public void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException + public void exportData(Context context, DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException { CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180); @@ -57,10 +59,12 @@ public class CsvDatabaseExporter implements DatabaseExporter DBHelper.LoyaltyCardDbIds.BALANCE, DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, DBHelper.LoyaltyCardDbIds.CARD_ID, - DBHelper.LoyaltyCardDbIds.HEADER_COLOR, DBHelper.LoyaltyCardDbIds.BARCODE_ID, DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, - DBHelper.LoyaltyCardDbIds.STAR_STATUS); + DBHelper.LoyaltyCardDbIds.HEADER_COLOR, + DBHelper.LoyaltyCardDbIds.STAR_STATUS, + CSVHelpers.IMAGE_FRONT, + CSVHelpers.IMAGE_BACK); Cursor cardCursor = db.getLoyaltyCardCursor(); @@ -75,10 +79,12 @@ public class CsvDatabaseExporter implements DatabaseExporter card.balance, card.balanceType, card.cardId, - card.headerColor, card.barcodeId, card.barcodeType, - card.starStatus); + card.headerColor, + card.starStatus, + Utils.bitmapToBase64(Utils.retrieveCardImage(context, card.id, true)), + Utils.bitmapToBase64(Utils.retrieveCardImage(context, card.id, false))); if(Thread.currentThread().isInterrupted()) { diff --git a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/CsvImporter.java similarity index 86% rename from app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java rename to app/src/main/java/protect/card_locker/importexport/CsvImporter.java index 6836cb5eb..a7c5876f7 100644 --- a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvImporter.java @@ -1,6 +1,10 @@ package protect.card_locker.importexport; +import android.content.Context; import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Base64; import com.google.zxing.BarcodeFormat; @@ -22,6 +26,7 @@ import java.util.List; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; import protect.card_locker.Group; +import protect.card_locker.Utils; /** * Class for importing a database from CSV (Comma Separate Values) @@ -30,9 +35,9 @@ import protect.card_locker.Group; * 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 CsvDatabaseImporter implements DatabaseImporter +public class CsvImporter implements Importer { - public void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException + public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); @@ -50,10 +55,10 @@ public class CsvDatabaseImporter implements DatabaseImporter switch (version) { case 1: - parseV1(db, bufferedReader); + parseV1(context, db, bufferedReader); break; case 2: - parseV2(db, bufferedReader); + parseV2(context, db, bufferedReader); break; default: throw new FormatException(String.format("No code to parse version %s", version)); @@ -62,7 +67,7 @@ public class CsvDatabaseImporter implements DatabaseImporter bufferedReader.close(); } - public void parseV1(DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException + public void parseV1(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException { final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.withHeader()); @@ -73,7 +78,7 @@ public class CsvDatabaseImporter implements DatabaseImporter { for (CSVRecord record : parser) { - importLoyaltyCard(database, db, record); + importLoyaltyCard(context, database, db, record); if(Thread.currentThread().isInterrupted()) { @@ -95,7 +100,7 @@ public class CsvDatabaseImporter implements DatabaseImporter } } - public void parseV2(DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException + public void parseV2(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException { SQLiteDatabase database = db.getWritableDatabase(); database.beginTransaction(); @@ -116,7 +121,7 @@ public class CsvDatabaseImporter implements DatabaseImporter parseV2Groups(db, database, stringPart); break; case 2: - parseV2Cards(db, database, stringPart); + parseV2Cards(context, db, database, stringPart); break; case 3: parseV2CardGroups(db, database, stringPart); @@ -164,14 +169,14 @@ public class CsvDatabaseImporter implements DatabaseImporter } } - public void parseV2Cards(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException + 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()); try { for (CSVRecord record : cardParser) { - importLoyaltyCard(database, db, record); + importLoyaltyCard(context, database, db, record); if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); @@ -208,7 +213,7 @@ public class CsvDatabaseImporter implements DatabaseImporter * Import a single loyalty card into the database using the given * session. */ - private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record) + private void importLoyaltyCard(Context context, SQLiteDatabase database, DBHelper helper, CSVRecord record) throws IOException, FormatException { int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false); @@ -270,11 +275,15 @@ public class CsvDatabaseImporter implements DatabaseImporter try { starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false); } catch (FormatException _e ) { - // This field did not exist in versions 0.28 and before + // This field did not exist in versions 0.278 and before // We catch this exception so we can still import old backups } if (starStatus != 1) starStatus = 0; + helper.insertLoyaltyCard(database, id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus); + + Utils.saveCardImage(context, CSVHelpers.extractImage(CSVHelpers.IMAGE_FRONT, record), id, true); + Utils.saveCardImage(context, CSVHelpers.extractImage(CSVHelpers.IMAGE_BACK, record), id, false); } /** diff --git a/app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java b/app/src/main/java/protect/card_locker/importexport/Exporter.java similarity index 66% rename from app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java rename to app/src/main/java/protect/card_locker/importexport/Exporter.java index a12a124af..37a341fc1 100644 --- a/app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/Exporter.java @@ -1,5 +1,7 @@ package protect.card_locker.importexport; +import android.content.Context; + import java.io.IOException; import java.io.OutputStreamWriter; @@ -9,11 +11,11 @@ import protect.card_locker.DBHelper; * Interface for a class which can export the contents of the database * in a given format. */ -public interface DatabaseExporter +public interface Exporter { /** * Export the database to the output stream in a given format. * @throws IOException */ - void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException; + void exportData(Context context, DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException; } 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 d7a1b84bf..8ff7a53ba 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -1,5 +1,6 @@ package protect.card_locker.importexport; +import android.content.Context; import android.database.sqlite.SQLiteDatabase; import com.google.zxing.BarcodeFormat; @@ -28,9 +29,9 @@ import protect.card_locker.FormatException; * 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 DatabaseImporter +public class FidmeImporter implements Importer { - public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { + public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { // We actually retrieve a .zip file ZipInputStream zipInputStream = new ZipInputStream(input); @@ -130,6 +131,8 @@ public class FidmeImporter implements DatabaseImporter // No favourite data in the export either int starStatus = 0; + // TODO: Front and back image + helper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, starStatus); } } \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/Importer.java similarity index 69% rename from app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java rename to app/src/main/java/protect/card_locker/importexport/Importer.java index 427b56535..9dec306e4 100644 --- a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/Importer.java @@ -1,5 +1,7 @@ package protect.card_locker.importexport; +import android.content.Context; + import org.json.JSONException; import java.io.IOException; @@ -13,7 +15,7 @@ import protect.card_locker.FormatException; * Interface for a class which can import the contents of a stream * into the database. */ -public interface DatabaseImporter +public interface Importer { /** * Import data from the input stream in a given format into @@ -21,5 +23,5 @@ public interface DatabaseImporter * @throws IOException * @throws FormatException */ - void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException; + void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException; } diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index ed0ee525b..f61636cc6 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -1,5 +1,6 @@ package protect.card_locker.importexport; +import android.content.Context; import android.util.Log; import java.io.IOException; @@ -22,14 +23,14 @@ public class MultiFormatExporter * false otherwise. If false, partial data may have been * written to the output stream, and it should be discarded. */ - public static boolean exportData(DBHelper db, OutputStreamWriter output, DataFormat format) + public static boolean exportData(Context context, DBHelper db, OutputStreamWriter output, DataFormat format) { - DatabaseExporter exporter = null; + Exporter exporter = null; switch(format) { case Catima: - exporter = new CsvDatabaseExporter(); + exporter = new CsvExporter(); break; default: Log.e(TAG, "Failed to export data, unknown format " + format.name()); @@ -40,7 +41,7 @@ public class MultiFormatExporter { try { - exporter.exportData(db, output); + exporter.exportData(context, db, output); return true; } catch(IOException e) diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 187ee03fb..34c620c74 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -1,5 +1,6 @@ package protect.card_locker.importexport; +import android.content.Context; import android.util.Log; import org.json.JSONException; @@ -27,14 +28,14 @@ public class MultiFormatImporter * false otherwise. If false, no data was written to * the database. */ - public static boolean importData(DBHelper db, InputStream input, DataFormat format) + public static boolean importData(Context context, DBHelper db, InputStream input, DataFormat format) { - DatabaseImporter importer = null; + Importer importer = null; switch(format) { case Catima: - importer = new CsvDatabaseImporter(); + importer = new CsvImporter(); break; case Fidme: importer = new FidmeImporter(); @@ -48,7 +49,7 @@ public class MultiFormatImporter { try { - importer.importData(db, input); + importer.importData(context, db, input); return true; } catch(IOException | FormatException | InterruptedException | JSONException | ParseException e) diff --git a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java index d66c8eea2..950ec0bc3 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -1,6 +1,7 @@ package protect.card_locker.importexport; import android.annotation.SuppressLint; +import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.graphics.Color; @@ -32,9 +33,9 @@ import protect.card_locker.FormatException; * 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 VoucherVaultImporter implements DatabaseImporter +public class VoucherVaultImporter implements Importer { - public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { + public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); @@ -122,7 +123,7 @@ public class VoucherVaultImporter implements DatabaseImporter headerColor = Color.YELLOW; break; default: - throw new FormatException("Unknown colour type foun: " + colorFromJSON); + throw new FormatException("Unknown colour type found: " + colorFromJSON); } db.insertLoyaltyCard(store, "", expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0); diff --git a/app/src/main/res/layout/loyalty_card_edit_activity.xml b/app/src/main/res/layout/loyalty_card_edit_activity.xml index cd156ebd9..619ee1fa7 100644 --- a/app/src/main/res/layout/loyalty_card_edit_activity.xml +++ b/app/src/main/res/layout/loyalty_card_edit_activity.xml @@ -1,7 +1,8 @@ @@ -38,6 +39,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/barcode"/> + @@ -326,6 +331,84 @@ android:layout_weight="1.0"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/loyalty_card_view_layout.xml b/app/src/main/res/layout/loyalty_card_view_layout.xml index db4c0124f..f9c2f9bed 100644 --- a/app/src/main/res/layout/loyalty_card_view_layout.xml +++ b/app/src/main/res/layout/loyalty_card_view_layout.xml @@ -134,60 +134,122 @@ android:id="@+id/bottom_sheet" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/inputBackground" + android:fitsSystemWindows="true" android:orientation="vertical" - android:padding="20dp" android:visibility="gone" app:behavior_hideable="false" app:behavior_peekHeight="104dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" - tools:visibility="visible" - android:fitsSystemWindows="true"> + tools:visibility="visible"> - + android:layout_height="match_parent"> - + - + - + + + + + + + + + + + + + + + + + + + + + + The value is not valid for the selected barcode type Copied card IDs to clipboard I want to share some cards with you + Card\'s front image + Card\'s back image + Photos + Set front image + Set back image + Remove image + Choose image from gallery + Take a photo Update barcode value? You changed the card ID. Do you want to also update the barcode to use the same value? Yes diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 3af2efa40..86c2d0534 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -3,8 +3,14 @@ package protect.card_locker; import android.app.Activity; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.os.Environment; +import android.util.DisplayMetrics; +import android.util.Log; import com.google.zxing.BarcodeFormat; @@ -16,6 +22,8 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowLog; +import org.robolectric.util.Util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -35,6 +43,9 @@ import java.util.Currency; import java.util.Date; import java.util.List; +import javax.annotation.Resource; + +import androidx.core.content.res.ResourcesCompat; import protect.card_locker.importexport.MultiFormatExporter; import protect.card_locker.importexport.MultiFormatImporter; @@ -59,6 +70,8 @@ public class ImportExportTest @Before public void setUp() { + ShadowLog.stream = System.out; + activity = Robolectric.setupActivity(MainActivity.class); db = TestHelpers.getEmptyDb(activity); nowMs = System.currentTimeMillis(); @@ -323,7 +336,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + boolean result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outStream, DataFormat.Catima); assertTrue(result); outStream.close(); @@ -332,7 +345,7 @@ public class ImportExportTest ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); // Import the CSV data - result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -354,7 +367,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + boolean result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outStream, DataFormat.Catima); assertTrue(result); outStream.close(); @@ -363,7 +376,7 @@ public class ImportExportTest ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); // Import the CSV data - result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -427,7 +440,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + boolean result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outStream, DataFormat.Catima); assertTrue(result); outStream.close(); @@ -436,7 +449,7 @@ public class ImportExportTest ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); // Import the CSV data - result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -471,14 +484,14 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export into CSV data - boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + boolean result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outStream, DataFormat.Catima); assertTrue(result); outStream.close(); ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); // Import the CSV data on top of the existing database - result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -502,7 +515,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + boolean result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outStream, DataFormat.Catima); assertTrue(result); TestHelpers.getEmptyDb(activity); @@ -516,7 +529,7 @@ public class ImportExportTest ByteArrayInputStream inData = new ByteArrayInputStream((outData.toString() + corruptEntry).getBytes()); // Attempt to import the data - result = MultiFormatImporter.importData(db, inData, format); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, format); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -602,7 +615,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -640,7 +653,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -678,7 +691,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -703,7 +716,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -741,7 +754,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -779,7 +792,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -817,7 +830,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -836,7 +849,7 @@ public class ImportExportTest inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -857,24 +870,37 @@ public class ImportExportTest } @Test - public void exportV2() + public void exportV2() throws FileNotFoundException { db.insertGroup("Example"); + + 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()); + + Bitmap frontImage = launcher.getBitmap(); + Bitmap backImage = roundLauncher.getBitmap(); + int loyaltyCard = (int) db.insertLoyaltyCard("Card 1", "Note 1", new Date(1618053234), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", BarcodeFormat.QR_CODE, 1, 0); + + Utils.saveCardImage(activity.getApplicationContext(), Utils.resizeBitmap(frontImage), loyaltyCard, true); + Utils.saveCardImage(activity.getApplicationContext(), Utils.resizeBitmap(backImage), loyaltyCard, false); + db.setLoyaltyCardGroups(loyaltyCard, Arrays.asList(db.getGroup("Example"))); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); - MultiFormatExporter.exportData(db, outputStreamWriter, DataFormat.Catima); + MultiFormatExporter.exportData(activity.getApplicationContext(), db, outputStreamWriter, DataFormat.Catima); String outputCsv = "2\r\n" + "\r\n" + "_id\r\n" + "Example\r\n" + "\r\n" + - "_id,store,note,expiry,balance,balancetype,cardid,headercolor,barcodeid,barcodetype,starstatus\r\n" + - "1,Card 1,Note 1,1618053234,100,USD,1234,1,5432,QR_CODE,0\r\n" + + "_id,store,note,expiry,balance,balancetype,cardid,barcodeid,barcodetype,headercolor,starstatus,frontimage,backimage\r\n" + + "1,Card 1,Note 1,1618053234,100,USD,1234,5432,QR_CODE,1,0,\"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIAAQAAAADcA-lXAAAANklEQVR42u3BAQEAAACCIP-vbkhA\n" + + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8G4IAAAFjdVCkAAAAAElFTkSuQmCC\n\",\"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIAAQAAAADcA-lXAAAANklEQVR42u3BAQEAAACCIP-vbkhA\n" + + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8G4IAAAFjdVCkAAAAAElFTkSuQmCC\n\"\r\n" + "\r\n" + "cardId,groupId\r\n" + "1,Example\r\n"; @@ -892,13 +918,16 @@ public class ImportExportTest "Food\n" + "Fashion\n" + "\n" + - "_id,store,note,expiry,balance,balancetype,cardid,barcodeid,headercolor,barcodetype,starstatus\n" + - "8,Clothes Store,Note about store,,0,,a,,-5317,,0\n" + - "2,Department Store,,1618041729,0,,A,,-9977996,,0\n" + - "3,Grocery Store,,,150,,dhd,,-9977996,,0\n" + - "4,Pharmacy,,,0,,dhshsvshs,,-10902850,,1\n" + - "5,Restaurant,Note about restaurant here,,0,,98765432,23456,-10902850,CODE_128,0\n" + - "6,Shoe Store,,,12.50,EUR,a,-5317,,AZTEC,0\n" + + "_id,store,note,expiry,balance,balancetype,cardid,barcodeid,headercolor,barcodetype,starstatus,frontimage,backimage\n" + + "1,Card 1,Note 1,1618053234,100,USD,1234,5432,1,QR_CODE,0,\"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIAAQAAAADcA-lXAAAANklEQVR42u3BAQEAAACCIP-vbkhA\n" + + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8G4IAAAFjdVCkAAAAAElFTkSuQmCC\n\",\"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIAAQAAAADcA-lXAAAANklEQVR42u3BAQEAAACCIP-vbkhA\n" + + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8G4IAAAFjdVCkAAAAAElFTkSuQmCC\n\"\r\n" + + "8,Clothes Store,Note about store,,0,,a,,-5317,,0,,\n" + + "2,Department Store,,1618041729,0,,A,,-9977996,,0,,\n" + + "3,Grocery Store,,,150,,dhd,,-9977996,,0,,\n" + + "4,Pharmacy,,,0,,dhshsvshs,,-10902850,,1,,\n" + + "5,Restaurant,Note about restaurant here,,0,,98765432,23456,-10902850,CODE_128,0,,\n" + + "6,Shoe Store,,,12.50,EUR,a,-5317,,AZTEC,0,,\n" + "\n" + "cardId,groupId\n" + "8,Fashion\n" + @@ -910,9 +939,9 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima); assertEquals(true, result); - assertEquals(6, db.getLoyaltyCardCount()); + assertEquals(7, db.getLoyaltyCardCount()); assertEquals(3, db.getGroupCount()); // Check all groups @@ -932,6 +961,27 @@ public class ImportExportTest assertEquals(Arrays.asList(8, 6), db.getGroupCardIds("Fashion")); // Check all cards + 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()); + + Bitmap frontImage = launcher.getBitmap(); + Bitmap backImage = roundLauncher.getBitmap(); + + LoyaltyCard card1 = db.getLoyaltyCard(1); + + assertEquals("Card 1", card1.store); + assertEquals("Note 1", card1.note); + assertEquals(new Date(1618053234), card1.expiry); + assertEquals(new BigDecimal("100"), card1.balance); + assertEquals(Currency.getInstance("USD"), card1.balanceType); + assertEquals("1234", card1.cardId); + assertEquals("5432", card1.barcodeId); + assertEquals(BarcodeFormat.QR_CODE, card1.barcodeType); + assertEquals(1, (long) card1.headerColor); + assertEquals(0, card1.starStatus); + assertTrue(Utils.resizeBitmap(frontImage).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, true))); + assertTrue(Utils.resizeBitmap(backImage).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, false))); + LoyaltyCard card8 = db.getLoyaltyCard(8); assertEquals("Clothes Store", card8.store); @@ -944,6 +994,8 @@ public class ImportExportTest assertEquals(null, card8.barcodeType); assertEquals(-5317, (long) card8.headerColor); assertEquals(0, card8.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, false)); LoyaltyCard card2 = db.getLoyaltyCard(2); @@ -957,6 +1009,8 @@ public class ImportExportTest assertEquals(null, card2.barcodeType); assertEquals(-9977996, (long) card2.headerColor); assertEquals(0, card2.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, false)); LoyaltyCard card3 = db.getLoyaltyCard(3); @@ -970,6 +1024,8 @@ public class ImportExportTest assertEquals(null, card3.barcodeType); assertEquals(-9977996, (long) card3.headerColor); assertEquals(0, card3.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, false)); LoyaltyCard card4 = db.getLoyaltyCard(4); @@ -983,6 +1039,8 @@ public class ImportExportTest assertEquals(null, card4.barcodeType); assertEquals(-10902850, (long) card4.headerColor); assertEquals(1, card4.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, false)); LoyaltyCard card5 = db.getLoyaltyCard(5); @@ -996,6 +1054,8 @@ public class ImportExportTest assertEquals(BarcodeFormat.CODE_128, card5.barcodeType); assertEquals(-10902850, (long) card5.headerColor); assertEquals(0, card5.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, false)); LoyaltyCard card6 = db.getLoyaltyCard(6); @@ -1009,6 +1069,8 @@ public class ImportExportTest assertEquals(BarcodeFormat.AZTEC, card6.barcodeType); assertEquals(null, card6.headerColor); assertEquals(0, card6.starStatus); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card6.id, true)); + assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card6.id, false)); TestHelpers.getEmptyDb(activity); } @@ -1041,7 +1103,7 @@ public class ImportExportTest ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonText.getBytes(StandardCharsets.UTF_8)); // Import the Voucher Vault data - boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.VoucherVault); + boolean result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.VoucherVault); assertTrue(result); assertEquals(2, db.getLoyaltyCardCount()); diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index ff5aa2193..d267e429e 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -9,7 +9,9 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; @@ -55,6 +57,7 @@ import static android.os.Looper.getMainLooper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.robolectric.Shadows.shadowOf; @@ -76,6 +79,13 @@ public class LoyaltyCardViewActivityTest ; } + enum FieldTypeView + { + TextView, + TextInputLayout, + ImageView + } + @Before public void setUp() { @@ -277,20 +287,23 @@ public class LoyaltyCardViewActivityTest } private void checkFieldProperties(final Activity activity, final int id, final int visibility, - final String contents) + final Object contents, final FieldTypeView fieldType) { final View view = activity.findViewById(id); assertNotNull(view); assertEquals(visibility, view.getVisibility()); - if(contents != null) - { - try { - TextView textView = (TextView) view; - assertEquals(contents, textView.getText().toString()); - } catch (ClassCastException e) { - TextInputLayout textView = (TextInputLayout) view; - assertEquals(contents, textView.getEditText().getText().toString()); - } + + if (fieldType == FieldTypeView.TextView) { + TextView textView = (TextView) view; + assertEquals(contents, textView.getText().toString()); + } else if (fieldType == FieldTypeView.TextInputLayout) { + TextInputLayout textView = (TextInputLayout) view; + assertEquals(contents, textView.getEditText().getText().toString()); + } else if (fieldType == FieldTypeView.ImageView) { + ImageView imageView = (ImageView) view; + // TODO: implement + } else { + throw new UnsupportedOperationException(); } } @@ -302,21 +315,23 @@ public class LoyaltyCardViewActivityTest { if(mode == ViewMode.VIEW_CARD) { - checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId); + checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId, FieldTypeView.TextView); } else { int editVisibility = View.VISIBLE; - checkFieldProperties(activity, R.id.storeNameEdit, editVisibility, store); - checkFieldProperties(activity, R.id.noteEdit, editVisibility, note); - checkFieldProperties(activity, R.id.expiryView, editVisibility, expiryString); - checkFieldProperties(activity, R.id.balanceField, editVisibility, balanceString); - checkFieldProperties(activity, R.id.balanceCurrencyField, editVisibility, balanceTypeString); - checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId); - checkFieldProperties(activity, R.id.barcodeIdField, View.VISIBLE, barcodeId); - checkFieldProperties(activity, R.id.barcodeTypeField, View.VISIBLE, barcodeType); - checkFieldProperties(activity, R.id.barcode, View.VISIBLE, null); + checkFieldProperties(activity, R.id.storeNameEdit, editVisibility, store, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.noteEdit, editVisibility, note, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.expiryView, editVisibility, expiryString, FieldTypeView.TextInputLayout); + checkFieldProperties(activity, R.id.balanceField, editVisibility, balanceString, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.balanceCurrencyField, editVisibility, balanceTypeString, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.barcodeIdField, View.VISIBLE, barcodeId, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.barcodeTypeField, View.VISIBLE, barcodeType, FieldTypeView.TextView); + checkFieldProperties(activity, R.id.barcode, View.VISIBLE, null, FieldTypeView.ImageView); + checkFieldProperties(activity, R.id.frontImage, View.VISIBLE, null, FieldTypeView.ImageView); + checkFieldProperties(activity, R.id.backImage, View.VISIBLE, null, FieldTypeView.ImageView); } } @@ -495,7 +510,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString()); db.close(); } @@ -533,12 +548,12 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString()); db.close(); } @@ -557,12 +572,12 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString()); // Cancel the loyalty card creation assertEquals(false, activity.isFinishing()); @@ -595,7 +610,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); // Set date to today MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField); @@ -609,7 +624,7 @@ public class LoyaltyCardViewActivityTest shadowOf(getMainLooper()).idle(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); db.close(); } @@ -628,13 +643,13 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); // Set date to never MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField); expiryField.setText(expiryField.getAdapter().getItem(0).toString(), false); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); db.close(); } @@ -653,7 +668,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); // Set balance to 10 points EditText balanceField = activity.findViewById(R.id.balanceField); @@ -704,7 +719,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "10.00", "$", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "10.00", "$", EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); shadowOf(getMainLooper()).idle(); @@ -726,7 +741,7 @@ public class LoyaltyCardViewActivityTest shadowOf(getMainLooper()).idle(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", "₩", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", "₩", EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString()); db.close(); } @@ -869,13 +884,13 @@ public class LoyaltyCardViewActivityTest activityController.resume(); // First check if the card is as expected - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString()); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString()); // Complete empty barcode selection successfully selectBarcodeWithResult(activity, BARCODE_DATA, "", true); // Check if the barcode type is NO_BARCODE as expected - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, null, context.getString(R.string.noBarcode)); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), context.getString(R.string.noBarcode)); assertEquals(View.GONE, activity.findViewById(R.id.barcodeLayout).getVisibility()); // Check if the special NO_BARCODE string doesn't get saved @@ -966,7 +981,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity) activityController.get(); DBHelper db = TestHelpers.getEmptyDb(activity); - db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, Color.BLACK,0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); activityController.resume(); @@ -1115,7 +1130,7 @@ public class LoyaltyCardViewActivityTest shadowOf(getMainLooper()).idle(); - checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", null, "AZTEC"); + checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", context.getString(R.string.sameAsCardId), "AZTEC"); assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor()); } @@ -1136,7 +1151,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = activity.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", null, "AZTEC"); + checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", context.getString(R.string.sameAsCardId), "AZTEC"); assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor()); } }