diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b316501466..b37a4fb6b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ import com.github.spotbugs.snom.SpotBugsTask plugins { id("com.android.application") id("com.github.spotbugs") + id("org.jetbrains.kotlin.android") } spotbugs { @@ -62,8 +63,8 @@ android { // Flag to enable support for the new language APIs isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } sourceSets { @@ -84,25 +85,26 @@ android { lint { lintConfig = file("lint.xml") } + kotlinOptions { + jvmTarget = "17" + } } dependencies { - // AndroidX implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.exifinterface:exifinterface:1.3.7") implementation("androidx.palette:palette:1.0.0") implementation("androidx.preference:preference:1.2.1") implementation("com.google.android.material:material:1.12.0") - implementation("com.github.yalantis:ucrop:2.2.9") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") - // Splash Screen - implementation("androidx.core:core-splashscreen:1.0.1") - // Third-party implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar") + implementation("com.github.yalantis:ucrop:2.2.9") implementation("com.google.zxing:core:3.5.3") implementation("org.apache.commons:commons-csv:1.9.0") implementation("com.jaredrummler:colorpicker:1.1.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a07cd9d18..ccf5521a80 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,12 +40,24 @@ - + + + + + + + + + + + + + createPublisherForAllAvailable() { Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived); return subscriber -> { while (loyaltyCardCursor.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(loyaltyCardCursor); + LoyaltyCard card = LoyaltyCard.fromCursor(this, loyaltyCardCursor); Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(LoyaltyCardViewActivity.BUNDLE_ID, card.id); @@ -69,7 +69,7 @@ public Flow.Publisher createPublisherFor(@NonNull List controlI for (String controlId : controlIds) { Control control; Integer cardId = this.controlIdToCardId(controlId); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, cardId); + LoyaltyCard card = DBHelper.getLoyaltyCard(this, mDatabase, cardId); if (card != null) { Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 7fd52962d3..927b2ef32a 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -332,10 +332,10 @@ public static Set imageFiles(Context context, final SQLiteDatabase datab Set files = new HashSet<>(); Cursor cardCursor = getLoyaltyCardCursor(database); while (cardCursor.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(cardCursor); + LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor); for (ImageLocationType imageLocationType : ImageLocationType.values()) { String name = Utils.getCardImageFileName(card.id, imageLocationType); - if (Utils.retrieveCardImageAsFile(context, name).exists()) { + if (card.getImageForImageLocationType(imageLocationType) != null) { files.add(name); } } @@ -535,14 +535,14 @@ public static boolean updateLoyaltyCardBalance(SQLiteDatabase database, final in return (rowsUpdated == 1); } - public static LoyaltyCard getLoyaltyCard(SQLiteDatabase database, final int id) { + public static LoyaltyCard getLoyaltyCard(Context context, SQLiteDatabase database, final int id) { Cursor data = database.query(LoyaltyCardDbIds.TABLE, null, whereAttrs(LoyaltyCardDbIds.ID), withArgs(id), null, null, null); LoyaltyCard card = null; if (data.getCount() == 1) { data.moveToFirst(); - card = LoyaltyCard.fromCursor(data); + card = LoyaltyCard.fromCursor(context, data); } data.close(); diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index b733d71d48..61bcff3bcb 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -125,7 +125,26 @@ public LoyaltyCard parse(Uri uri) throws InvalidObjectException { headerColor = Integer.parseInt(unparsedHeaderColor); } - return new LoyaltyCard(-1, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100, 0); + return new LoyaltyCard( + -1, + store, + note, + validFrom, + expiry, + balance, + balanceType, + cardId, + barcodeId, + barcodeType, + headerColor, + 0, + Utils.getUnixTime(), + 100, + 0, + null, + null, + null + ); } catch (NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) { throw new InvalidObjectException("Not a valid import URI"); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index d4e3bf5317..d258c9347c 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -1,6 +1,8 @@ package protect.card_locker; +import android.content.Context; import android.database.Cursor; +import android.graphics.Bitmap; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -11,8 +13,7 @@ import java.math.BigDecimal; import java.util.Currency; import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.Objects; public class LoyaltyCard implements Parcelable { @@ -38,6 +39,13 @@ public class LoyaltyCard implements Parcelable { public int zoomLevel; public int archiveStatus; + @Nullable + public Bitmap imageThumbnail; + @Nullable + public Bitmap imageFront; + @Nullable + public Bitmap imageBack; + public static final String BUNDLE_LOYALTY_CARD_ID = "loyaltyCardId"; public static final String BUNDLE_LOYALTY_CARD_STORE = "loyaltyCardStore"; public static final String BUNDLE_LOYALTY_CARD_NOTE = "loyaltyCardNote"; @@ -53,6 +61,9 @@ public class LoyaltyCard implements Parcelable { public static final String BUNDLE_LOYALTY_CARD_LAST_USED = "loyaltyCardLastUsed"; public static final String BUNDLE_LOYALTY_CARD_ZOOM_LEVEL = "loyaltyCardZoomLevel"; public static final String BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS = "loyaltyCardArchiveStatus"; + public static final String BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL = "loyaltyCardImageThumbnail"; + public static final String BUNDLE_LOYALTY_CARD_IMAGE_FRONT = "loyaltyCardImageFront"; + public static final String BUNDLE_LOYALTY_CARD_IMAGE_BACK = "loyaltyCardImageBack"; /** * Create a loyalty card object with default values @@ -73,6 +84,9 @@ public LoyaltyCard() { setLastUsed(Utils.getUnixTime()); setZoomLevel(100); setArchiveStatus(0); + setImageThumbnail(null); + setImageFront(null); + setImageBack(null); } /** @@ -98,7 +112,8 @@ public LoyaltyCard(final int id, final String store, final String note, @Nullabl @Nullable final Date expiry, final BigDecimal balance, @Nullable final Currency balanceType, final String cardId, @Nullable final String barcodeId, @Nullable final CatimaBarcode barcodeType, @Nullable final Integer headerColor, final int starStatus, - final long lastUsed, final int zoomLevel, final int archiveStatus) { + final long lastUsed, final int zoomLevel, final int archiveStatus, + @Nullable Bitmap imageThumbnail, @Nullable Bitmap imageFront, @Nullable Bitmap imageBack) { setId(id); setStore(store); setNote(note); @@ -114,6 +129,9 @@ public LoyaltyCard(final int id, final String store, final String note, @Nullabl setLastUsed(lastUsed); setZoomLevel(zoomLevel); setArchiveStatus(archiveStatus); + setImageThumbnail(imageThumbnail); + setImageFront(imageFront); + setImageBack(imageBack); } public void setId(int id) { @@ -188,6 +206,31 @@ public void setArchiveStatus(int archiveStatus) { this.archiveStatus = archiveStatus; } + public void setImageThumbnail(@Nullable Bitmap imageThumbnail) { + this.imageThumbnail = imageThumbnail; + } + + public void setImageFront(@Nullable Bitmap imageFront) { + this.imageFront = imageFront; + } + + public void setImageBack(@Nullable Bitmap imageBack) { + this.imageBack = imageBack; + } + + @Nullable + public Bitmap getImageForImageLocationType(ImageLocationType imageLocationType) { + if (imageLocationType == ImageLocationType.icon) { + return imageThumbnail; + } else if (imageLocationType == ImageLocationType.front) { + return imageFront; + } else if (imageLocationType == ImageLocationType.back) { + return imageBack; + } + + throw new IllegalArgumentException("Unknown image location type"); + } + protected LoyaltyCard(Parcel in) { setId(in.readInt()); setStore(Objects.requireNonNull(in.readString())); @@ -208,6 +251,9 @@ protected LoyaltyCard(Parcel in) { setLastUsed(in.readLong()); setZoomLevel(in.readInt()); setArchiveStatus(in.readInt()); + setImageThumbnail(in.readParcelable(Bitmap.class.getClassLoader())); + setImageFront(in.readParcelable(Bitmap.class.getClassLoader())); + setImageBack(in.readParcelable(Bitmap.class.getClassLoader())); } @Override @@ -227,8 +273,14 @@ public void writeToParcel(Parcel parcel, int flags) { parcel.writeLong(lastUsed); parcel.writeInt(zoomLevel); parcel.writeInt(archiveStatus); + // FIXME: This is way too much data, it causes random crashes: "Could not copy bitmap to parcel blob". + // The bundle/parcelable should probably contain links to some temp file instead. + parcel.writeParcelable(imageThumbnail, 0); + parcel.writeParcelable(imageFront, 0); + parcel.writeParcelable(imageBack, 0); } + @NonNull public static LoyaltyCard fromBundle(Bundle bundle, boolean requireFull) { // Grab default card LoyaltyCard loyaltyCard = new LoyaltyCard(); @@ -240,7 +292,7 @@ public static LoyaltyCard fromBundle(Bundle bundle, boolean requireFull) { return loyaltyCard; } - public void updateFromBundle(Bundle bundle, boolean requireFull) { + public void updateFromBundle(@NonNull Bundle bundle, boolean requireFull) { if (bundle.containsKey(BUNDLE_LOYALTY_CARD_ID)) { setId(bundle.getInt(BUNDLE_LOYALTY_CARD_ID)); } else if (requireFull) { @@ -321,41 +373,89 @@ public void updateFromBundle(Bundle bundle, boolean requireFull) { } else if (requireFull) { throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS); } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { + setImageThumbnail(bundle.getParcelable(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)); + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL); + } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) { + setImageFront(bundle.getParcelable(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)); + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_FRONT); + } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) { + setImageBack(bundle.getParcelable(BUNDLE_LOYALTY_CARD_IMAGE_BACK)); + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_BACK); + } } - public Bundle toBundle() { + public Bundle toBundle(List exportLimit) { + boolean exportIsLimited = !exportLimit.isEmpty(); + Bundle bundle = new Bundle(); - bundle.putInt(BUNDLE_LOYALTY_CARD_ID, id); - bundle.putString(BUNDLE_LOYALTY_CARD_STORE, store); - bundle.putString(BUNDLE_LOYALTY_CARD_NOTE, note); - if (validFrom != null) { - bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom.getTime()); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ID)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_ID, id); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STORE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_STORE, store); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_NOTE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_NOTE, note); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_VALID_FROM)) { + bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom != null ? validFrom.getTime() : -1); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_EXPIRY)) { + bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry != null ? expiry.getTime() : -1); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BALANCE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE, balance.toString()); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BALANCE_TYPE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE_TYPE, balanceType != null ? balanceType.toString() : null); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_CARD_ID)) { + bundle.putString(BUNDLE_LOYALTY_CARD_CARD_ID, cardId); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_ID)) { + bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_ID, barcodeId); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_TYPE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor != null ? headerColor : -1); } - if (expiry != null) { - bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry.getTime()); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STAR_STATUS)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_STAR_STATUS, starStatus); } - bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE, balance.toString()); - if (balanceType != null) { - bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE_TYPE, balanceType.toString()); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_LAST_USED)) { + bundle.putLong(BUNDLE_LOYALTY_CARD_LAST_USED, lastUsed); } - bundle.putString(BUNDLE_LOYALTY_CARD_CARD_ID, cardId); - bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_ID, barcodeId); - if (barcodeType != null) { - bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_TYPE, barcodeType.name()); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL, zoomLevel); } - if (headerColor != null) { - bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS, archiveStatus); + } + // FIXME: This is way too much data, it causes random crashes: "Could not copy bitmap to parcel blob". + // The bundle/parcelable should probably contain links to some temp file instead. + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { + bundle.putParcelable(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, imageThumbnail); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) { + bundle.putParcelable(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, imageFront); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) { + bundle.putParcelable(BUNDLE_LOYALTY_CARD_IMAGE_BACK, imageBack); } - bundle.putInt(BUNDLE_LOYALTY_CARD_STAR_STATUS, starStatus); - bundle.putLong(BUNDLE_LOYALTY_CARD_LAST_USED, lastUsed); - bundle.putInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL, zoomLevel); - bundle.putInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS, archiveStatus); return bundle; } - public static LoyaltyCard fromCursor(Cursor cursor) { + public static LoyaltyCard fromCursor(Context context, Cursor cursor) { // id int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); // store @@ -392,11 +492,37 @@ public static LoyaltyCard fromCursor(Cursor cursor) { int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL)); // archiveStatus int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS)); - - return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus, lastUsed, zoomLevel, archiveStatus); + // imageThumbnail + Bitmap imageThumbnail = Utils.retrieveCardImage(context, id, ImageLocationType.icon); + // imageFront + Bitmap imageFront = Utils.retrieveCardImage(context, id, ImageLocationType.front); + // imageBack + Bitmap imageBack = Utils.retrieveCardImage(context, id, ImageLocationType.back); + + return new LoyaltyCard( + id, + store, + note, + validFrom, + expiry, + balance, + balanceType, + cardId, + barcodeId, + barcodeType, + headerColor, + starStatus, + lastUsed, + zoomLevel, + archiveStatus, + imageThumbnail, + imageFront, + imageBack + ); } public static boolean isDuplicate(final LoyaltyCard a, final LoyaltyCard b) { + // Note: Bitmap comparing is slow, be careful when calling this method // Skip lastUsed & zoomLevel return a.id == b.id && // non-nullable int a.store.equals(b.store) && // non-nullable String @@ -411,7 +537,23 @@ public static boolean isDuplicate(final LoyaltyCard a, final LoyaltyCard b) { b.barcodeType == null ? null : b.barcodeType.format()) && // nullable CatimaBarcode with no overridden .equals(), so we need to check .format() Utils.equals(a.headerColor, b.headerColor) && // nullable Integer a.starStatus == b.starStatus && // non-nullable int - a.archiveStatus == b.archiveStatus; // non-nullable int + a.archiveStatus == b.archiveStatus && // non-nullable int + nullableBitmapsEqual(a.imageThumbnail, b.imageThumbnail) && // nullable Bitmap + nullableBitmapsEqual(a.imageFront, b.imageFront) && // nullable Bitmap + nullableBitmapsEqual(a.imageBack, b.imageBack); // nullable Bitmap + } + + public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap b) { + if (a == null && b == null) { + return true; + } + + if (a != null && b != null) { + return a.sameAs(b); + } + + // One is null and the other isn't, so it's not equal + return false; } @Override @@ -425,7 +567,8 @@ public String toString() { return String.format( "LoyaltyCard{%n id=%s,%n store=%s,%n note=%s,%n validFrom=%s,%n expiry=%s,%n" + " balance=%s,%n balanceType=%s,%n cardId=%s,%n barcodeId=%s,%n barcodeType=%s,%n" - + " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n archiveStatus=%s%n}", + + " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n archiveStatus=%s%n" + + " imageThumbnail=%s,%n imageFront=%s,%n imageBack=%s,%n}", this.id, this.store, this.note, @@ -440,11 +583,14 @@ public String toString() { this.starStatus, this.lastUsed, this.zoomLevel, - this.archiveStatus + this.archiveStatus, + this.imageThumbnail, + this.imageFront, + this.imageBack ); } - public static final Creator CREATOR = new Creator() { + public static final Creator CREATOR = new Creator<>() { @Override public LoyaltyCard createFromParcel(Parcel in) { return new LoyaltyCard(in); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index f43aece3d4..446df08bf4 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -80,7 +80,7 @@ public LoyaltyCardListItemViewHolder onCreateViewHolder(@NonNull ViewGroup input public LoyaltyCard getCard(int position) { mCursor.moveToPosition(position); - return LoyaltyCard.fromCursor(mCursor); + return LoyaltyCard.fromCursor(mContext, mCursor); } public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) { @@ -88,7 +88,7 @@ public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor i boolean showDivider = false; inputHolder.mDivider.setVisibility(View.GONE); - LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(inputCursor); + LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(mContext, inputCursor); Bitmap icon = Utils.retrieveCardImage(mContext, loyaltyCard.id, ImageLocationType.icon); if (mLoyaltyCardListDisplayOptions.showingNameBelowThumbnail() && icon != null) { @@ -193,7 +193,7 @@ public ArrayList getSelectedItems() { int i; for (i = 0; i < mSelectedItems.size(); i++) { mCursor.moveToPosition(mSelectedItems.keyAt(i)); - result.add(LoyaltyCard.fromCursor(mCursor)); + result.add(LoyaltyCard.fromCursor(mContext, mCursor)); } return result; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index fdafa2963c..db4f322be8 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -94,14 +94,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private final String STATE_TEMP_CARD = "tempLoyaltyCard"; private final String STATE_TEMP_CARD_FIELD = "tempLoyaltyCardField"; private final String STATE_REQUESTED_IMAGE = "requestedImage"; - private final String STATE_FRONT_IMAGE_UNSAVED = "frontImageUnsaved"; - private final String STATE_BACK_IMAGE_UNSAVED = "backImageUnsaved"; - private final String STATE_ICON_UNSAVED = "iconUnsaved"; private final String STATE_UPDATE_LOYALTY_CARD = "updateLoyaltyCard"; private final String STATE_HAS_CHANGED = "hasChange"; - private final String STATE_FRONT_IMAGE_REMOVED = "frontImageRemoved"; - private final String STATE_BACK_IMAGE_REMOVED = "backImageRemoved"; - private final String STATE_ICON_REMOVED = "iconRemoved"; private final String STATE_OPEN_SET_ICON_MENU = "openSetIconMenu"; private static final String PICK_DATE_REQUEST_KEY = "pick_date_request"; @@ -111,11 +105,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private final String TEMP_CROP_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_crop_image.png"; private final Bitmap.CompressFormat TEMP_CROP_IMAGE_FORMAT = Bitmap.CompressFormat.PNG; - private final String TEMP_UNSAVED_FRONT_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_front_image.png"; - private final String TEMP_UNSAVED_BACK_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_back_image.png"; - private final String TEMP_UNSAVED_ICON_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_icon.png"; - private final Bitmap.CompressFormat TEMP_UNSAVED_IMAGE_FORMAT = Bitmap.CompressFormat.PNG; - private static final int PERMISSION_REQUEST_CAMERA_IMAGE_FRONT = 100; private static final int PERMISSION_REQUEST_CAMERA_IMAGE_BACK = 101; private static final int PERMISSION_REQUEST_CAMERA_IMAGE_ICON = 102; @@ -185,17 +174,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements ActivityResultLauncher mCropperLauncher; int mRequestedImage = 0; - int mCropperFinishedType = 0; UCrop.Options mCropperOptions; - boolean mFrontImageUnsaved = false; - boolean mBackImageUnsaved = false; - boolean mIconUnsaved = false; - - boolean mFrontImageRemoved = false; - boolean mBackImageRemoved = false; - boolean mIconRemoved = false; - final private TaskHandler mTasks = new TaskHandler(); // store system locale for Build.VERSION.SDK_INT < Build.VERSION_CODES.N @@ -288,7 +268,7 @@ private boolean extractIntentFields(Intent intent) { // If we have to import a loyalty card, do so if (updateLoyaltyCard || duplicateFromLoyaltyCardId) { - tempLoyaltyCard = DBHelper.getLoyaltyCard(mDatabase, loyaltyCardId); + tempLoyaltyCard = DBHelper.getLoyaltyCard(this, mDatabase, loyaltyCardId); if (tempLoyaltyCard == null) { Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); @@ -323,34 +303,9 @@ public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { savedInstanceState.putInt(STATE_TAB_INDEX, tabs.getSelectedTabPosition()); savedInstanceState.putParcelable(STATE_TEMP_CARD, tempLoyaltyCard); savedInstanceState.putSerializable(STATE_TEMP_CARD_FIELD, tempLoyaltyCardField); - savedInstanceState.putInt(STATE_REQUESTED_IMAGE, mRequestedImage); - - Object cardImageFrontObj = cardImageFront.getTag(); - if (mFrontImageUnsaved && (cardImageFrontObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) cardImageFrontObj, TEMP_UNSAVED_FRONT_IMAGE_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { - savedInstanceState.putInt(STATE_FRONT_IMAGE_UNSAVED, 1); - } else { - savedInstanceState.putInt(STATE_FRONT_IMAGE_UNSAVED, 0); - } - - Object cardImageBackObj = cardImageBack.getTag(); - if (mBackImageUnsaved && (cardImageBackObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) cardImageBackObj, TEMP_UNSAVED_BACK_IMAGE_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { - savedInstanceState.putInt(STATE_BACK_IMAGE_UNSAVED, 1); - } else { - savedInstanceState.putInt(STATE_BACK_IMAGE_UNSAVED, 0); - } - - Object thumbnailObj = thumbnail.getTag(); - if (mIconUnsaved && (thumbnailObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) thumbnailObj, TEMP_UNSAVED_ICON_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { - savedInstanceState.putInt(STATE_ICON_UNSAVED, 1); - } else { - savedInstanceState.putInt(STATE_ICON_UNSAVED, 0); - } savedInstanceState.putInt(STATE_UPDATE_LOYALTY_CARD, updateLoyaltyCard ? 1 : 0); savedInstanceState.putInt(STATE_HAS_CHANGED, hasChanged ? 1 : 0); - savedInstanceState.putInt(STATE_FRONT_IMAGE_REMOVED, mFrontImageRemoved ? 1 : 0); - savedInstanceState.putInt(STATE_BACK_IMAGE_REMOVED, mBackImageRemoved ? 1 : 0); - savedInstanceState.putInt(STATE_ICON_REMOVED, mIconRemoved ? 1 : 0); savedInstanceState.putInt(STATE_OPEN_SET_ICON_MENU, openSetIconMenu ? 1 : 0); } @@ -362,15 +317,8 @@ public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); tabs = binding.tabs; tabs.selectTab(tabs.getTabAt(savedInstanceState.getInt(STATE_TAB_INDEX))); - mRequestedImage = savedInstanceState.getInt(STATE_REQUESTED_IMAGE); - mFrontImageUnsaved = savedInstanceState.getInt(STATE_FRONT_IMAGE_UNSAVED) == 1; - mBackImageUnsaved = savedInstanceState.getInt(STATE_BACK_IMAGE_UNSAVED) == 1; - mIconUnsaved = savedInstanceState.getInt(STATE_ICON_UNSAVED) == 1; updateLoyaltyCard = savedInstanceState.getInt(STATE_UPDATE_LOYALTY_CARD) == 1; hasChanged = savedInstanceState.getInt(STATE_HAS_CHANGED) == 1; - mFrontImageRemoved = savedInstanceState.getInt(STATE_FRONT_IMAGE_REMOVED) == 1; - mBackImageRemoved = savedInstanceState.getInt(STATE_BACK_IMAGE_REMOVED) == 1; - mIconRemoved = savedInstanceState.getInt(STATE_ICON_REMOVED) == 1; openSetIconMenu = savedInstanceState.getInt(STATE_OPEN_SET_ICON_MENU) == 1; } @@ -425,7 +373,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { setLoyaltyCardStore(storeName); generateIcon(storeName); - if (storeName.length() == 0) { + if (storeName.isEmpty()) { storeFieldEdit.setError(getString(R.string.field_must_not_be_empty)); } else { storeFieldEdit.setError(null); @@ -693,27 +641,21 @@ public void onTabReselected(TabLayout.Tab tab) { mCardIdAndBarCodeEditorLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK) { - Intent intent = result.getData(); - if (intent == null) { - Log.d("barcode card id editor", "barcode and card id editor picker returned without an intent"); + Intent resultIntent = result.getData(); + if (resultIntent == null) { + Log.d(TAG, "barcode and card id editor picker returned without an intent"); return; } - List barcodeValuesList = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, getApplicationContext()); - - Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { - @Override - public void onUserChoseBarcode(BarcodeValues barcodeValues) { - setLoyaltyCardCardId(barcodeValues.content()); - setLoyaltyCardBarcodeType(barcodeValues.format()); - setLoyaltyCardBarcodeId(""); - } - - @Override - public void onUserDismissedSelector() { + Bundle resultIntentBundle = resultIntent.getExtras(); + if (resultIntentBundle == null) { + Log.d(TAG, "barcode and card id editor picker returned without a bundle"); + return; + } - } - }); + tempLoyaltyCard.updateFromBundle(resultIntentBundle, false); + generateBarcode(); + hasChanged = true; } }); @@ -733,20 +675,13 @@ public void onUserDismissedSelector() { if (bitmap != null) { if (requestedFrontImage()) { - mFrontImageRemoved = false; - mFrontImageUnsaved = true; - setCardImage(cardImageFront, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); + setCardImage(ImageLocationType.front, cardImageFront, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); } else if (requestedBackImage()) { - mBackImageRemoved = false; - mBackImageUnsaved = true; - setCardImage(cardImageBack, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); + setCardImage(ImageLocationType.back, cardImageBack, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); } else { - mIconRemoved = false; - mIconUnsaved = true; setThumbnailImage(Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_SMALL)); } Log.d("cropper", "mRequestedImage: " + mRequestedImage); - mCropperFinishedType = mRequestedImage; hasChanged = true; } else { Toast.makeText(LoyaltyCardEditActivity.this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); @@ -819,26 +754,14 @@ private boolean requestedFrontImage() { return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_FRONT; } - private boolean croppedFrontImage() { - return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_FRONT; - } - private boolean requestedBackImage() { return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_BACK || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_BACK; } - private boolean croppedBackImage() { - return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_BACK || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_BACK; - } - private boolean requestedIcon() { return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_ICON || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_ICON; } - private boolean croppedIcon() { - return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_ICON || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_ICON; - } - @SuppressLint("DefaultLocale") @Override protected void onResume() { @@ -848,40 +771,12 @@ protected void onResume() { onResuming = true; - if (!initDone) { - if (updateLoyaltyCard) { - setTitle(R.string.editCardTitle); - } else { - setTitle(R.string.addCardTitle); - } - - if (updateLoyaltyCard || duplicateFromLoyaltyCardId) { - if (!mFrontImageUnsaved && !croppedFrontImage() && !mFrontImageRemoved) { - setCardImage(cardImageFront, Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.front), true); - } - if (!mBackImageUnsaved && !croppedBackImage() && !mBackImageRemoved) { - setCardImage(cardImageBack, Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.back), true); - } - if (!mIconUnsaved && !croppedIcon() && !mIconRemoved) { - setThumbnailImage(Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.icon)); - } - } else { - setTitle(R.string.addCardTitle); - } - - if (mFrontImageUnsaved && !croppedFrontImage()) { - setCardImage(cardImageFront, Utils.loadTempImage(this, TEMP_UNSAVED_FRONT_IMAGE_NAME), true); - } - if (mBackImageUnsaved && !croppedBackImage()) { - setCardImage(cardImageBack, Utils.loadTempImage(this, TEMP_UNSAVED_BACK_IMAGE_NAME), true); - } - if (mIconUnsaved && !croppedIcon()) { - setThumbnailImage(Utils.loadTempImage(this, TEMP_UNSAVED_ICON_NAME)); - } + if (updateLoyaltyCard) { + setTitle(R.string.editCardTitle); + } else { + setTitle(R.string.addCardTitle); } - mCropperFinishedType = 0; - boolean hadChanges = hasChanged; storeFieldEdit.setText(tempLoyaltyCard.store); @@ -967,6 +862,10 @@ protected void onResume() { } } + setThumbnailImage(tempLoyaltyCard.imageThumbnail); + setCardImage(ImageLocationType.front, cardImageFront, tempLoyaltyCard.imageFront, true); + setCardImage(ImageLocationType.back, cardImageBack, tempLoyaltyCard.imageBack, true); + // Initialization has finished if (!initDone) { initDone = true; @@ -1006,9 +905,8 @@ protected void onResume() { } protected void setColorFromIcon() { - Object icon = thumbnail.getTag(); - if (icon != null && (icon instanceof Bitmap)) { - int headerColor = Utils.getHeaderColorFromImage((Bitmap) icon, Utils.getHeaderColor(this, tempLoyaltyCard)); + if (tempLoyaltyCard.imageThumbnail != null) { + int headerColor = Utils.getHeaderColorFromImage(tempLoyaltyCard.imageThumbnail, Utils.getHeaderColor(this, tempLoyaltyCard)); setLoyaltyCardHeaderColor(headerColor); @@ -1020,7 +918,7 @@ protected void setColorFromIcon() { } protected void setThumbnailImage(@Nullable Bitmap bitmap) { - setCardImage(thumbnail, bitmap, false); + setCardImage(ImageLocationType.icon, thumbnail, bitmap, false); if (bitmap == null) { generateIcon(storeFieldEdit.getText().toString().trim()); @@ -1035,8 +933,16 @@ protected void setThumbnailImage(@Nullable Bitmap bitmap) { } } - protected void setCardImage(ImageView imageView, Bitmap bitmap, boolean applyFallback) { - imageView.setTag(bitmap); + protected void setCardImage(ImageLocationType imageLocationType, ImageView imageView, Bitmap bitmap, boolean applyFallback) { + if (imageLocationType == ImageLocationType.icon) { + tempLoyaltyCard.setImageThumbnail(bitmap); + } else if (imageLocationType == ImageLocationType.front) { + tempLoyaltyCard.setImageFront(bitmap); + } else if (imageLocationType == ImageLocationType.back) { + tempLoyaltyCard.setImageBack(bitmap); + } else { + throw new IllegalArgumentException("Unknown image type"); + } if (bitmap != null) { imageView.setImageBitmap(bitmap); @@ -1293,30 +1199,30 @@ public void onClick(View v) { class ChooseCardImage implements View.OnClickListener { @Override public void onClick(View v) throws NoSuchElementException { + Bitmap currentImage; + ImageLocationType imageLocationType; ImageView targetView; if (v.getId() == R.id.frontImageHolder) { + currentImage = tempLoyaltyCard.imageFront; + imageLocationType = ImageLocationType.front; targetView = cardImageFront; } else if (v.getId() == R.id.backImageHolder) { + currentImage = tempLoyaltyCard.imageBack; + imageLocationType = ImageLocationType.back; targetView = cardImageBack; } else if (v.getId() == R.id.thumbnail) { + currentImage = tempLoyaltyCard.imageThumbnail; + imageLocationType = ImageLocationType.icon; targetView = thumbnail; } else { throw new IllegalArgumentException("Invalid IMAGE ID " + v.getId()); } LinkedHashMap> cardOptions = new LinkedHashMap<>(); - if (targetView.getTag() != null && v.getId() != R.id.thumbnail) { + if (currentImage != null && v.getId() != R.id.thumbnail) { cardOptions.put(getString(R.string.removeImage), () -> { - if (targetView == cardImageFront) { - mFrontImageRemoved = true; - mFrontImageUnsaved = false; - } else { - mBackImageRemoved = true; - mBackImageUnsaved = false; - } - - setCardImage(targetView, null, true); + setCardImage(imageLocationType, targetView, null, true); return null; }); } @@ -1372,21 +1278,17 @@ public void onClick(View v) throws NoSuchElementException { }); if (v.getId() == R.id.thumbnail) { - if (cardImageFront.getTag() instanceof Bitmap) { + if (tempLoyaltyCard.imageFront != null) { cardOptions.put(getString(R.string.useFrontImage), () -> { - mIconRemoved = false; - mIconUnsaved = true; - setThumbnailImage(Utils.resizeBitmap((Bitmap) cardImageFront.getTag(), Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(tempLoyaltyCard.imageFront, Utils.BITMAP_SIZE_SMALL)); return null; }); } - if (cardImageBack.getTag() instanceof Bitmap) { + if (tempLoyaltyCard.imageBack != null) { cardOptions.put(getString(R.string.useBackImage), () -> { - mIconRemoved = false; - mIconUnsaved = true; - setThumbnailImage(Utils.resizeBitmap((Bitmap) cardImageBack.getTag(), Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(tempLoyaltyCard.imageBack, Utils.BITMAP_SIZE_SMALL)); return null; }); @@ -1437,8 +1339,6 @@ public void onColorSelected(int dialogId, int color) { setLoyaltyCardHeaderColor(color); // Unset image if set - mIconRemoved = true; - mIconUnsaved = false; setThumbnailImage(null); } @@ -1616,16 +1516,16 @@ private void doSave() { } try { - Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, ImageLocationType.front); - Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, ImageLocationType.back); - Utils.saveCardImage(this, (Bitmap) thumbnail.getTag(), loyaltyCardId, ImageLocationType.icon); + Utils.saveCardImage(this, tempLoyaltyCard.imageFront, loyaltyCardId, ImageLocationType.front); + Utils.saveCardImage(this, tempLoyaltyCard.imageBack, loyaltyCardId, ImageLocationType.back); + Utils.saveCardImage(this, tempLoyaltyCard.imageThumbnail, loyaltyCardId, ImageLocationType.icon); } catch (FileNotFoundException e) { e.printStackTrace(); } DBHelper.setLoyaltyCardGroups(mDatabase, loyaltyCardId, selectedGroups); - ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(mDatabase, loyaltyCardId)); + ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, loyaltyCardId)); if (duplicateFromLoyaltyCardId) { Intent intent = new Intent(getApplicationContext(), MainActivity.class); @@ -1759,7 +1659,7 @@ private void generateIcon(String store) { return; } - if (thumbnail.getTag() == null) { + if (tempLoyaltyCard.imageThumbnail == null) { thumbnail.setBackgroundColor(tempLoyaltyCard.headerColor); LetterBitmap letterBitmap = Utils.generateIcon(this, store, tempLoyaltyCard.headerColor); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 71d898d2ec..fdd0c65947 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -660,7 +660,7 @@ protected void onResume() { window.setAttributes(attributes); } - loyaltyCard = DBHelper.getLoyaltyCard(database, loyaltyCardId); + loyaltyCard = DBHelper.getLoyaltyCard(this, database, loyaltyCardId); if (loyaltyCard == null) { Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index a8e97c2985..78f0880afd 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -7,6 +7,7 @@ import android.content.SharedPreferences; import android.database.CursorIndexOutOfBoundsException; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.DisplayMetrics; @@ -257,12 +258,9 @@ public void onTabReselected(TabLayout.Tab tab) { return; } - Intent intent = result.getData(); - List barcodeValuesList = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, this); - - Bundle inputBundle = intent.getExtras(); - String group = inputBundle != null ? inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null; - processBarcodeValuesList(barcodeValuesList, group, false); + Intent editIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); + editIntent.putExtras(result.getData().getExtras()); + startActivity(editIntent); }); mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { @@ -422,21 +420,16 @@ private void updateLoyaltyCardList(boolean updateCount) { } } - private void processBarcodeValuesList(List barcodeValuesList, String group, boolean closeAppOnNoBarcode) { - if (barcodeValuesList.isEmpty()) { - throw new IllegalArgumentException("barcodesValues may not be empty"); + private void processParseResultList(List parseResultList, String group, boolean closeAppOnNoBarcode) { + if (parseResultList.isEmpty()) { + throw new IllegalArgumentException("parseResultList may not be empty"); } - Utils.makeUserChooseBarcodeFromList(MainActivity.this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { + Utils.makeUserChooseParseResultFromList(MainActivity.this, parseResultList, new ParseResultListDisambiguatorCallback() { @Override - public void onUserChoseBarcode(BarcodeValues barcodeValues) { - CatimaBarcode barcodeType = barcodeValues.format(); - + public void onUserChoseParseResult(ParseResult parseResult) { Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); - Bundle bundle = new Bundle(); - bundle.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, barcodeValues.content()); - bundle.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null); - bundle.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_ID, null); + Bundle bundle = parseResult.toLoyaltyCardBundle(); if (group != null) { bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group); } @@ -457,28 +450,48 @@ private void onSharedIntent(Intent intent) { String receivedAction = intent.getAction(); String receivedType = intent.getType(); - // Check if an image or file was shared to us - if (Intent.ACTION_SEND.equals(receivedAction)) { - List barcodeValuesList; + if (receivedAction == null || receivedType == null) { + return; + } + + List parseResultList; - if (receivedType.equals("text/plain")) { - barcodeValuesList = Collections.singletonList(new BarcodeValues(null, intent.getStringExtra(Intent.EXTRA_TEXT))); - } else if (receivedType.startsWith("image/")) { - barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); - } else if (receivedType.equals("application/pdf")) { - barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + // Check for shared text + if (receivedAction.equals(Intent.ACTION_SEND) && receivedType.equals("text/plain")) { + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)); + parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); + } else { + // Parse whatever file was sent, regardless of opening or sharing + Uri data; + if (receivedAction.equals(Intent.ACTION_VIEW)) { + data = intent.getData(); + } else if (receivedAction.equals(Intent.ACTION_SEND)) { + data = intent.getParcelableExtra(Intent.EXTRA_STREAM); } else { - Log.e(TAG, "Wrong mime-type"); + Log.e(TAG, "Wrong action type to parse intent"); return; } - if (barcodeValuesList.isEmpty()) { - finish(); + if (receivedType.startsWith("image/")) { + parseResultList = Utils.retrieveBarcodesFromImage(this, data); + } else if (receivedType.equals("application/pdf")) { + parseResultList = Utils.retrieveBarcodesFromPdf(this, data); + } else if (receivedType.equals("application/vnd.apple.pkpass")) { + parseResultList = Utils.retrieveBarcodesFromPkPass(this, data); + } else { + Log.e(TAG, "Wrong mime-type"); return; } + } - processBarcodeValuesList(barcodeValuesList, null, true); + // Give up if we should parse but there is nothing to parse + if (parseResultList == null || parseResultList.isEmpty()) { + finish(); + return; } + + processParseResultList(parseResultList, null, true); } private void extractIntentFields(Intent intent) { diff --git a/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java index 258d5ddec5..e0f41e0cbe 100644 --- a/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java @@ -33,7 +33,7 @@ public void swapCursor(Cursor inputCursor) { @Override public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) { - LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(inputCursor); + LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(mContext, inputCursor); Boolean overlayValue = mInGroupOverlay.get(loyaltyCard.id); if ((overlayValue != null ? overlayValue : isLoyaltyCardInGroup(loyaltyCard.id))) { mAnimationItemsIndex.put(inputCursor.getPosition(), true); diff --git a/app/src/main/java/protect/card_locker/ParseResult.kt b/app/src/main/java/protect/card_locker/ParseResult.kt new file mode 100644 index 0000000000..174c0ac97f --- /dev/null +++ b/app/src/main/java/protect/card_locker/ParseResult.kt @@ -0,0 +1,29 @@ +package protect.card_locker + +import android.os.Bundle + +class ParseResult( + val parseResultType: ParseResultType, + val loyaltyCard: LoyaltyCard) { + var note: String? = null + + fun toLoyaltyCardBundle(): Bundle { + when (parseResultType) { + ParseResultType.FULL -> return loyaltyCard.toBundle(listOf()) + ParseResultType.BARCODE_ONLY -> { + val defaultLoyaltyCard = LoyaltyCard() + defaultLoyaltyCard.setBarcodeId(null) + defaultLoyaltyCard.setBarcodeType(loyaltyCard.barcodeType) + defaultLoyaltyCard.setCardId(loyaltyCard.cardId) + + return defaultLoyaltyCard.toBundle( + listOf( + LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_ID, + LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_TYPE, + LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/ParseResultListDisambiguatorCallback.java b/app/src/main/java/protect/card_locker/ParseResultListDisambiguatorCallback.java new file mode 100644 index 0000000000..50acba5847 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ParseResultListDisambiguatorCallback.java @@ -0,0 +1,6 @@ +package protect.card_locker; + +public interface ParseResultListDisambiguatorCallback { + void onUserChoseParseResult(ParseResult parseResult); + void onUserDismissedSelector(); +} diff --git a/app/src/main/java/protect/card_locker/ParseResultType.kt b/app/src/main/java/protect/card_locker/ParseResultType.kt new file mode 100644 index 0000000000..6314b53cb7 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ParseResultType.kt @@ -0,0 +1,6 @@ +package protect.card_locker + +enum class ParseResultType { + FULL, + BARCODE_ONLY +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/PkpassParser.kt b/app/src/main/java/protect/card_locker/PkpassParser.kt new file mode 100644 index 0000000000..c56e945da8 --- /dev/null +++ b/app/src/main/java/protect/card_locker/PkpassParser.kt @@ -0,0 +1,401 @@ +package protect.card_locker + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.util.ArrayMap +import android.util.Log +import com.google.zxing.BarcodeFormat +import net.lingala.zip4j.io.inputstream.ZipInputStream +import net.lingala.zip4j.model.LocalFileHeader +import org.json.JSONException +import org.json.JSONObject +import java.io.FileNotFoundException +import java.io.IOException +import java.math.BigDecimal +import java.text.DateFormat +import java.text.ParseException +import java.time.ZonedDateTime +import java.time.format.DateTimeParseException +import java.util.Currency +import java.util.Date + +class PkpassParser(context: Context, uri: Uri?) { + private var mContext = context + + private var translations: ArrayMap> = ArrayMap() + + private var passContent: JSONObject? = null + + private var store: String? = null + private var note: String? = null + private var validFrom: Date? = null + private var expiry: Date? = null + private val balance: BigDecimal = BigDecimal(0) + private val balanceType: Currency? = null + private var cardId: String? = null + private var barcodeId: String? = null + private var barcodeType: CatimaBarcode? = null + private var headerColor: Int? = null + private val starStatus = 0 + private val lastUsed: Long = 0 + private val zoomLevel = DBHelper.DEFAULT_ZOOM_LEVEL + private var archiveStatus = 0 + + var image: Bitmap? = null + private set + private var logoSize = 0 + + init { + if (passContent != null) { + throw IllegalStateException("Pkpass instance already initialized!") + } + + mContext = context + + Log.i(TAG, "Received Pkpass file") + if (uri == null) { + Log.e(TAG, "Uri did not contain any data") + throw IOException(context.getString(R.string.errorReadingFile)) + } + + try { + mContext.contentResolver.openInputStream(uri).use { inputStream -> + ZipInputStream(inputStream).use { zipInputStream -> + var localFileHeader: LocalFileHeader + while ((zipInputStream.nextEntry.also { localFileHeader = it }) != null) { + // Ignore directories + if (localFileHeader.isDirectory) continue + + // We assume there are three options, as per spec: + // language.lproj/pass.strings + // file.extension + // More directories are ignored + val filenameParts = localFileHeader.fileName.split('/') + if (filenameParts.size > 2) { + continue + } else if (filenameParts.size == 2) { + // Doesn't seem like a language directory, ignore + if (!filenameParts[0].endsWith(".lproj")) continue + + val locale = filenameParts[0].removeSuffix(".lproj") + + translations[locale] = parseLanguageStrings(ZipUtils.read(zipInputStream)) + } + + // Not a language, parse as normal files + when (localFileHeader.fileName) { + "logo.png" -> loadImageIfBiggerSize(1, zipInputStream) + "logo@2x.png" -> loadImageIfBiggerSize(2, zipInputStream) + "logo@3x.png" -> loadImageIfBiggerSize(3, zipInputStream) + "pass.json" -> passContent = ZipUtils.readJSON(zipInputStream) // Parse this last, so we're sure we have all language info + } + } + + checkNotNull(passContent) { "File lacks pass.json" } + } + } + } catch (e: FileNotFoundException) { + throw IOException(mContext.getString(R.string.errorReadingFile)) + } catch (e: Exception) { + throw e + } + } + + fun listLocales(): List { + return translations.keys.toList() + } + + fun toLoyaltyCard(locale: String?): LoyaltyCard { + parsePassJSON(checkNotNull(passContent) { "Pkpass instance not yet initialized!" }, locale) + + return LoyaltyCard( + -1, + store, + note, + validFrom, + expiry, + balance, + balanceType, + cardId, + barcodeId, + barcodeType, + headerColor, + starStatus, + lastUsed, + zoomLevel, + archiveStatus, + image, + null, + null + ) + } + + private fun getTranslation(string: String, locale: String?): String { + if (locale == null) { + return string + } + + val localeStrings = translations[locale] + + return localeStrings?.get(string) ?: string + } + + private fun loadImageIfBiggerSize(fileLogoSize: Int, zipInputStream: ZipInputStream) { + if (logoSize < fileLogoSize) { + image = ZipUtils.readImage(zipInputStream) + logoSize = fileLogoSize + } + } + + private fun parseDateTime(dateTime: String): Date { + return Date.from(ZonedDateTime.parse(dateTime).toInstant()) + } + + private fun parseLanguageStrings(data: String): Map { + val output = ArrayMap() + + // Translations look like this: + // "key_name" = "Translated value"; + // + // However, "Translated value" may be multiple lines and may contain " (however, it'll be escaped) + var translationLine = StringBuilder() + + for (line in data.lines()) { + translationLine.append(line) + + // Make sure we don't have a false ending (this is the escaped double quote: \";) + if (!line.endsWith("\\\";") and line.endsWith("\";")) { + // We reached a translation ending, time to parse it + + // 1. Split into key and value + // 2. Remove cruft of each + // 3. Clean up escape sequences + val keyValue = translationLine.toString().split("=", ignoreCase = false, limit = 2) + val key = keyValue[0].trim().removePrefix("\"").removeSuffix("\"") + val value = keyValue[1].trim().removePrefix("\"").removeSuffix("\";").replace("\\", "") + + output[key] = value + + translationLine = StringBuilder() + } else { + translationLine.append("\n") + } + } + + return output + } + + private fun parsePassJSON(jsonObject: JSONObject, locale: String?) { + if (jsonObject.getInt("formatVersion") != 1) { + throw IllegalArgumentException(mContext.getString(R.string.unsupportedFile)) + } + + // Prefer logoText for store, it's generally shorter + try { + store = jsonObject.getString("logoText") + } catch (ignored: JSONException) {} + + if (store.isNullOrEmpty()) { + store = jsonObject.getString("organizationName") + } + + val noteText = StringBuilder() + noteText.append(getTranslation(jsonObject.getString("description"), locale)) + + try { + validFrom = parseDateTime(jsonObject.getString("relevantDate")) + } catch (ignored: JSONException) {} + + try { + expiry = parseDateTime(jsonObject.getString("expirationDate")) + } catch (ignored: JSONException) {} + + try { + headerColor = Color.parseColor(jsonObject.getString("backgroundColor")) + } catch (ignored: JSONException) { + } catch (ignored: ParseException) {} + + var pkPassHasBarcodes = false + var validBarcodeFound = false + + // Create a list of possible barcodes + val barcodes = ArrayList() + + // Append the non-deprecated entries + try { + val foundInBarcodesField = jsonObject.getJSONArray("barcodes") + + for (i in 0 until foundInBarcodesField.length()) { + barcodes.add(foundInBarcodesField.getJSONObject(i)) + } + } catch (ignored: JSONException) {} + + // Append the deprecated entry if it exists + try { + barcodes.add(jsonObject.getJSONObject("barcode")) + } catch (ignored: JSONException) {} + + for (barcode in barcodes) { + pkPassHasBarcodes = true + + try { + parsePassJSONBarcodeField(barcode) + + validBarcodeFound = true + break + } catch (ignored: IllegalArgumentException) {} + } + + if (pkPassHasBarcodes && !validBarcodeFound) { + throw FormatException(mContext.getString(R.string.errorReadingFile)) + } + + // An used card being "archived" probably is the most sensible way to map "voided" + archiveStatus = try { + if (jsonObject.getBoolean("voided")) 1 else 0 + } catch (ignored: JSONException) { + 0 + } + + // Append type-specific info to the pass + noteText.append("\n\n") + + // Find the relevant pass type and parse it + var hasPassData = false + for (passType in listOf("boardingPass", "coupon", "eventTicket", "generic")) { + try { + noteText.append( + parsePassJSONPassFields( + jsonObject.getJSONObject(passType), + locale + ) + ) + + hasPassData = true + + break + } catch (ignored: JSONException) {} + } + + // Failed to parse anything, error out + if (!hasPassData) { + throw FormatException(mContext.getString(R.string.errorReadingFile)) + } + + note = noteText.toString() + } + + /* Return success or failure */ + private fun parsePassJSONBarcodeField(barcodeInfo: JSONObject) { + val format = barcodeInfo.getString("format") + + // We only need to check these 4 formats as no other options are valid in the PkPass spec + barcodeType = when(format) { + "PKBarcodeFormatQR" -> CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE) + "PKBarcodeFormatPDF417" -> CatimaBarcode.fromBarcode(BarcodeFormat.PDF_417) + "PKBarcodeFormatAztec" -> CatimaBarcode.fromBarcode(BarcodeFormat.AZTEC) + "PKBarcodeFormatCode128" -> CatimaBarcode.fromBarcode(BarcodeFormat.CODE_128) + else -> throw IllegalArgumentException("No valid barcode type") + } + + // FIXME: We probably need to do something with the messageEncoding field + try { + cardId = barcodeInfo.getString("altText") + barcodeId = barcodeInfo.getString("message") + } catch (ignored: JSONException) { + cardId = barcodeInfo.getString("message") + barcodeId = null + } + + // Don't set barcodeId if it's the same as cardId + if (cardId == barcodeId) { + barcodeId = null + } + } + + private fun parsePassJSONPassFields(fieldsParent: JSONObject, locale: String?): String { + // These fields contain a lot of info on where we're supposed to display them, but Catima doesn't really have anything for that + // So for now, throw them all into the description field in a logical order + val noteContents: MutableList = ArrayList() + + // Collect all the groups of fields that exist + for (fieldType in listOf("headerFields", "primaryFields", "secondaryFields", "auxiliaryFields", "backFields")) { + val content = StringBuilder() + + try { + val fieldArray = fieldsParent.getJSONArray(fieldType) + for (i in 0 until fieldArray.length()) { + val entry = fieldArray.getJSONObject(i) + + content.append(parsePassJSONPassField(entry, locale)) + + // If this is not the last part, add spacing on the end + if (i < (fieldArray.length() - 1)) { + content.append("\n") + } + } + } catch (ignore: JSONException) { + } catch (ignore: ParseException) { + } + + if (content.isNotEmpty()) { + noteContents.add(content.toString()) + } + } + + // Merge all field groups together, one paragraph for field group + val output = StringBuilder() + + for (i in 0 until noteContents.size) { + output.append(noteContents[i]) + + // If this is not the last part, add newlines to separate + if (i < (noteContents.size - 1)) { + output.append("\n\n") + } + } + + return output.toString() + } + + private fun parsePassJSONPassField(field: JSONObject, locale: String?): String { + // Value may be a localizable string, a date or a number. So let's try to parse it as a date first + + var value = getTranslation(field.getString("value"), locale) + try { + value = DateFormat.getDateTimeInstance().format(parseDateTime(value)) + } catch (ignored: DateTimeParseException) { + // It's fine if it's not a date + } + + // FIXME: Use the Android thing for formatted strings here + if (field.has("currencyCode")) { + val valueCurrency = Currency.getInstance(field.getString("currencyCode")) + + value = Utils.formatBalance( + mContext, + Utils.parseBalance(value, valueCurrency), + valueCurrency + ) + } else if (field.has("numberStyle")) { + if (field.getString("numberStyle") == "PKNumberStylePercent") { + // FIXME: Android formatting string + value = "${value}%" + } + } + + val label = getTranslation(field.getString("label"), locale) + + if (label.isNotEmpty()) { + return "$label: $value" + } + + return value + } + + companion object { + private const val TAG = "Catima" + } +} diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index dff50567b6..f2b7ef1351 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -67,6 +67,7 @@ public class ScanActivity extends CatimaAppCompatActivity { private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100; private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101; + private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102; private CaptureManager capture; private DecoratedBarcodeView barcodeScannerView; @@ -79,6 +80,7 @@ public class ScanActivity extends CatimaAppCompatActivity { // can't use the pre-made contract because that launches the file manager for image type instead of gallery private ActivityResultLauncher photoPickerLauncher; private ActivityResultLauncher pdfPickerLauncher; + private ActivityResultLauncher pkpassPickerLauncher; static final String STATE_SCANNER_ACTIVE = "scannerActive"; private boolean mScannerActive = true; @@ -107,6 +109,7 @@ protected void onCreate(Bundle savedInstanceState) { manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData())); photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData())); pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData())); + pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData())); customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> { setScannerActive(false); @@ -116,12 +119,14 @@ protected void onCreate(Bundle savedInstanceState) { getString(R.string.addManually), getString(R.string.addFromImage), getString(R.string.addFromPdfFile), + getString(R.string.addFromPkpass) }; Object[] icons = new Object[]{ R.drawable.baseline_block_24, R.drawable.ic_edit, R.drawable.baseline_image_24, R.drawable.baseline_picture_as_pdf_24, + R.drawable.local_activity_24px }; String[] columns = new String[]{"text", "icon"}; @@ -156,7 +161,10 @@ protected void onCreate(Bundle savedInstanceState) { addFromImage(); break; case 3: - addFromPdfFile(); + addFromPdf(); + break; + case 4: + addFromPkPass(); break; default: throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option"); @@ -181,16 +189,11 @@ protected void onCreate(Bundle savedInstanceState) { barcodeScannerView.decodeSingle(new BarcodeCallback() { @Override public void barcodeResult(BarcodeResult result) { - Intent scanResult = new Intent(); - Bundle scanResultBundle = new Bundle(); - scanResultBundle.putString(BARCODE_CONTENTS, result.getText()); - scanResultBundle.putString(BARCODE_FORMAT, result.getBarcodeFormat().name()); - if (addGroup != null) { - scanResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup); - } - scanResult.putExtras(scanResultBundle); - ScanActivity.this.setResult(RESULT_OK, scanResult); - finish(); + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setCardId(result.getText()); + loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat())); + + returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); } @Override @@ -294,35 +297,31 @@ private void setScannerActive(boolean isActive) { mScannerActive = isActive; } - private void returnResult(String barcodeContents, String barcodeFormat) { - Intent manualResult = new Intent(); - Bundle manualResultBundle = new Bundle(); - manualResultBundle.putString(BARCODE_CONTENTS, barcodeContents); - manualResultBundle.putString(BARCODE_FORMAT, barcodeFormat); + private void returnResult(ParseResult parseResult) { + Intent result = new Intent(); + Bundle bundle = parseResult.toLoyaltyCardBundle(); if (addGroup != null) { - manualResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup); + bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup); } - manualResult.putExtras(manualResultBundle); - ScanActivity.this.setResult(RESULT_OK, manualResult); + result.putExtras(bundle); + ScanActivity.this.setResult(RESULT_OK, result); finish(); } private void handleActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); - List barcodeValuesList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); + List parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); - if (barcodeValuesList.isEmpty()) { + if (parseResultList.isEmpty()) { setScannerActive(true); return; } - Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { + Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() { @Override - public void onUserChoseBarcode(BarcodeValues barcodeValues) { - CatimaBarcode barcodeType = barcodeValues.format(); - - returnResult(barcodeValues.content(), barcodeType != null ? barcodeType.name() : null); + public void onUserChoseParseResult(ParseResult parseResult) { + returnResult(parseResult); } @Override @@ -369,7 +368,9 @@ private void addWithoutBarcode() { // Buttons builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> { - returnResult(input.getText().toString(), null); + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setCardId(input.getText().toString()); + returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); }); builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel()); AlertDialog dialog = builder.create(); @@ -418,10 +419,14 @@ public void addFromImage() { PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE); } - public void addFromPdfFile() { + public void addFromPdf() { PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF); } + public void addFromPkPass() { + PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS); + } + private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher launcher, int chooserText, int errorMessage) { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType(mimeType); @@ -511,12 +516,14 @@ public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] } else { showCameraPermissionMissingText(); } - } else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF) { + } else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) { if (granted) { if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) { addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker); - } else { + } else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) { addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager); + } else { + addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager); } } else { setScannerActive(true); diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index 36a28aa2dc..77cd7c7462 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -87,7 +87,7 @@ static void updateShortcuts(Context context, LoyaltyCard card) { for (int index = 0; index < list.size(); index++) { ShortcutInfoCompat prevShortcut = list.get(index); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(database, Integer.parseInt(prevShortcut.getId())); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, Integer.parseInt(prevShortcut.getId())); // skip outdated cards that no longer exist if (loyaltyCard != null) { diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index d44fddb71a..81f6be7b38 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -78,6 +78,7 @@ import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -95,12 +96,13 @@ public class Utils { public static final int BARCODE_SCAN = 3; public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4; public static final int BARCODE_IMPORT_FROM_PDF_FILE = 5; - public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 6; - public static final int CARD_IMAGE_FROM_CAMERA_BACK = 7; - public static final int CARD_IMAGE_FROM_CAMERA_ICON = 8; - public static final int CARD_IMAGE_FROM_FILE_FRONT = 9; - public static final int CARD_IMAGE_FROM_FILE_BACK = 10; - public static final int CARD_IMAGE_FROM_FILE_ICON = 11; + public static final int BARCODE_IMPORT_FROM_PKPASS_FILE = 6; + public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 7; + public static final int CARD_IMAGE_FROM_CAMERA_BACK = 8; + public static final int CARD_IMAGE_FROM_CAMERA_ICON = 9; + public static final int CARD_IMAGE_FROM_FILE_FRONT = 10; + public static final int CARD_IMAGE_FROM_FILE_BACK = 11; + public static final int CARD_IMAGE_FROM_FILE_ICON = 12; public static final String CARD_IMAGE_FILENAME_REGEX = "^(card_)(\\d+)(_(?:front|back|icon)\\.png)$"; @@ -143,7 +145,7 @@ static public boolean needsDarkForeground(Integer backgroundColor) { return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT; } - static public List retrieveBarcodesFromImage(Context context, Uri uri) { + static public List retrieveBarcodesFromImage(Context context, Uri uri) { Log.i(TAG, "Received image file with possible barcode"); if (uri == null) { @@ -162,7 +164,7 @@ static public List retrieveBarcodesFromImage(Context context, Uri return new ArrayList<>(); } - List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); + List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); if (barcodesFromBitmap.isEmpty()) { Log.i(TAG, "No barcode found in image file"); @@ -172,7 +174,33 @@ static public List retrieveBarcodesFromImage(Context context, Uri return barcodesFromBitmap; } - static public List retrieveBarcodesFromPdf(Context context, Uri uri) { + static public List retrieveBarcodesFromPkPass(Context context, Uri uri) { + // FIXME: Also return image + Log.i(TAG, "Received Pkpass file with possible barcode"); + if (uri == null) { + Log.e(TAG, "Pkpass did not contain any data"); + Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show(); + return null; + } + + PkpassParser pkpassParser = new PkpassParser(context, uri); + + List locales = pkpassParser.listLocales(); + if (locales.isEmpty()) { + return Collections.singletonList(new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(null))); + } + + List parseResultList = new ArrayList<>(); + for (String locale : locales) { + ParseResult parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(locale)); + parseResult.setNote(locale); + parseResultList.add(parseResult); + } + + return parseResultList; + } + + static public List retrieveBarcodesFromPdf(Context context, Uri uri) { Log.i(TAG, "Received PDF file with possible barcode"); if (uri == null) { Log.e(TAG, "Uri did not contain any data"); @@ -182,7 +210,7 @@ static public List retrieveBarcodesFromPdf(Context context, Uri u ParcelFileDescriptor parcelFileDescriptor = null; PdfRenderer renderer = null; - List barcodesFromPdfPages = new ArrayList<>(); + List barcodesFromPdfPages = new ArrayList<>(); try { parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r"); @@ -194,13 +222,14 @@ static public List retrieveBarcodesFromPdf(Context context, Uri u for (int i = 0; i < renderer.getPageCount(); i++) { PdfRenderer.Page page = renderer.openPage(i); renderedPage = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888); + page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); - List barcodesFromPage = getBarcodesFromBitmap(renderedPage); - for (BarcodeValues barcodeValues : barcodesFromPage) { - barcodeValues.setNote(String.format(context.getString(R.string.pageWithNumber), i+1)); - barcodesFromPdfPages.add(barcodeValues); + List barcodesFromPage = getBarcodesFromBitmap(renderedPage); + for (ParseResult parseResult : barcodesFromPage) { + parseResult.setNote(String.format(context.getString(R.string.pageWithNumber), i+1)); + barcodesFromPdfPages.add(parseResult); } } } @@ -229,17 +258,17 @@ static public List retrieveBarcodesFromPdf(Context context, Uri u } /** - * Returns the Barcode format and content based on the result of an activity. - * It shows toasts to notify the end-user as needed itself and will return an empty - * BarcodeValues object if the activity was cancelled or nothing could be found. + * Returns the ParseResult based on the result of an activity. + * It shows toasts to notify the end-user as needed itself and will return an empty list if the + * activity was cancelled or nothing could be found. * * @param requestCode * @param resultCode * @param intent * @param context - * @return BarcodeValues + * @return List */ - static public List parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) { + static public List parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) { String contents; String format; @@ -255,6 +284,10 @@ static public List parseSetBarcodeActivityResult(int requestCode, return retrieveBarcodesFromPdf(context, intent.getData()); } + if (requestCode == Utils.BARCODE_IMPORT_FROM_PKPASS_FILE) { + return retrieveBarcodesFromPkPass(context, intent.getData()); + } + if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) { if (requestCode == Utils.BARCODE_SCAN) { Log.i(TAG, "Received barcode information from camera"); @@ -268,7 +301,15 @@ static public List parseSetBarcodeActivityResult(int requestCode, Log.i(TAG, "Read barcode id: " + contents); Log.i(TAG, "Read format: " + format); - return Collections.singletonList(new BarcodeValues(format != null ? CatimaBarcode.fromName(format) : null, contents)); + LoyaltyCard loyaltyCard = new LoyaltyCard(); + if (format != null) { + loyaltyCard.setBarcodeType(CatimaBarcode.fromName(format)); + } + if (contents != null) { + loyaltyCard.setCardId(contents); + } + + return Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); } throw new UnsupportedOperationException("Unknown request code for parseSetBarcodeActivityResult"); @@ -288,7 +329,7 @@ private static Bitmap getBitmapSdkLessThan29(Uri data, Context context) throws I return MediaStore.Images.Media.getBitmap(context.getContentResolver(), data); } - static public List getBarcodesFromBitmap(Bitmap bitmap) { + static public List getBarcodesFromBitmap(Bitmap bitmap) { // This function is vulnerable to OOM, so we try again with a smaller bitmap is we get OOM for (int i = 0; i < 10; i++) { try { @@ -303,7 +344,7 @@ static public List getBarcodesFromBitmap(Bitmap bitmap) { return new ArrayList<>(); } - static private List getBarcodesFromBitmapReal(Bitmap bitmap) { + static private List getBarcodesFromBitmapReal(Bitmap bitmap) { // In order to decode it, the Bitmap must first be converted into a pixel array... int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()]; bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); @@ -312,7 +353,7 @@ static private List getBarcodesFromBitmapReal(Bitmap bitmap) { LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray); BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); - List barcodeValuesList = new ArrayList<>(); + List parseResultList = new ArrayList<>(); try { MultiFormatReader multiFormatReader = new MultiFormatReader(); MultipleBarcodeReader multipleBarcodeReader = new GenericMultipleBarcodeReader(multiFormatReader); @@ -323,37 +364,42 @@ static private List getBarcodesFromBitmapReal(Bitmap bitmap) { Log.i(TAG, "Read barcode id: " + barcodeResult.getText()); Log.i(TAG, "Read format: " + barcodeResult.getBarcodeFormat().name()); - barcodeValuesList.add(new BarcodeValues(CatimaBarcode.fromBarcode(barcodeResult.getBarcodeFormat()), barcodeResult.getText())); + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setCardId(barcodeResult.getText()); + loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(barcodeResult.getBarcodeFormat())); + parseResultList.add(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); } - return barcodeValuesList; + return parseResultList; } catch (NotFoundException e) { - return barcodeValuesList; + return parseResultList; } } - static public void makeUserChooseBarcodeFromList(Context context, List barcodeValuesList, BarcodeValuesListDisambiguatorCallback callback) { + static public void makeUserChooseParseResultFromList(Context context, List parseResultList, ParseResultListDisambiguatorCallback callback) { // If there is only one choice, consider it chosen - if (barcodeValuesList.size() == 1) { - callback.onUserChoseBarcode(barcodeValuesList.get(0)); + if (parseResultList.size() == 1) { + callback.onUserChoseParseResult(parseResultList.get(0)); return; } // Ask user to choose a barcode // TODO: This should contain an image of the barcode in question to help users understand the choice they're making - CharSequence[] barcodeDescriptions = new CharSequence[barcodeValuesList.size()]; - for (int i = 0; i < barcodeValuesList.size(); i++) { - BarcodeValues barcodeValues = barcodeValuesList.get(i); - CatimaBarcode catimaBarcode = barcodeValues.format(); + CharSequence[] barcodeDescriptions = new CharSequence[parseResultList.size()]; + for (int i = 0; i < parseResultList.size(); i++) { + ParseResult parseResult = parseResultList.get(i); + CatimaBarcode catimaBarcode = parseResult.getLoyaltyCard().barcodeType; - String barcodeContent = barcodeValues.content(); + String barcodeContent = parseResult.getLoyaltyCard().cardId; // Shorten overly long barcodes if (barcodeContent.length() > 22) { barcodeContent = barcodeContent.substring(0, 20) + "…"; } - if (barcodeValues.note() != null) { - barcodeDescriptions[i] = String.format("%s: %s (%s)", barcodeValues.note(), catimaBarcode != null ? catimaBarcode.prettyName() : context.getString(R.string.noBarcode), barcodeContent); + String parseResultNote = parseResult.getNote(); + + if (parseResultNote != null) { + barcodeDescriptions[i] = String.format("%s: %s (%s)", parseResultNote, catimaBarcode != null ? catimaBarcode.prettyName() : context.getString(R.string.noBarcode), barcodeContent); } else { barcodeDescriptions[i] = String.format("%s (%s)", catimaBarcode != null ? catimaBarcode.prettyName() : context.getString(R.string.noBarcode), barcodeContent); } @@ -363,7 +409,7 @@ static public void makeUserChooseBarcodeFromList(Context context, List callback.onUserChoseBarcode(barcodeValuesList.get(i)) + (dialogInterface, i) -> callback.onUserChoseParseResult(parseResultList.get(i)) ); builder.setOnCancelListener(dialogInterface -> callback.onUserDismissedSelector()); builder.show(); @@ -869,7 +915,7 @@ public static int resolveBackgroundColor(AppCompatActivity activity) { return typedValue.data; } - public static int getHeaderColorFromImage(Bitmap image, int fallback) { + public static int getHeaderColorFromImage(@Nullable Bitmap image, int fallback) { if (image == null) { return fallback; } diff --git a/app/src/main/java/protect/card_locker/ZipUtils.java b/app/src/main/java/protect/card_locker/ZipUtils.java index c0c30bd1d6..158223dd91 100644 --- a/app/src/main/java/protect/card_locker/ZipUtils.java +++ b/app/src/main/java/protect/card_locker/ZipUtils.java @@ -23,7 +23,7 @@ public static JSONObject readJSON(ZipInputStream zipInputStream) throws IOExcept return new JSONObject(read(zipInputStream)); } - private static String read(ZipInputStream zipInputStream) throws IOException { + public static String read(ZipInputStream zipInputStream) throws IOException { StringBuilder stringBuilder = new StringBuilder(); Reader reader = new BufferedReader(new InputStreamReader(zipInputStream, StandardCharsets.UTF_8)); int c; diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java index 530f9c194b..c56e75666f 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java @@ -49,7 +49,7 @@ public void exportData(Context context, SQLiteDatabase database, OutputStream ou // Generate CSV ByteArrayOutputStream catimaOutputStream = new ByteArrayOutputStream(); OutputStreamWriter catimaOutputStreamWriter = new OutputStreamWriter(catimaOutputStream, StandardCharsets.UTF_8); - writeCSV(database, catimaOutputStreamWriter); + writeCSV(context, database, catimaOutputStreamWriter); // Add CSV to zip file ZipParameters csvZipParameters = createZipParameters("catima.csv", password); @@ -64,12 +64,12 @@ public void exportData(Context context, SQLiteDatabase database, OutputStream ou Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database); while (cardCursor.moveToNext()) { // For each card - LoyaltyCard card = LoyaltyCard.fromCursor(cardCursor); + LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor); // For each image for (ImageLocationType imageLocationType : ImageLocationType.values()) { // If it exists, add to the .zip file - Bitmap image = Utils.retrieveCardImage(context, card.id, imageLocationType); + Bitmap image = card.getImageForImageLocationType(imageLocationType); if (image != null) { ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password); zipOutputStream.putNextEntry(imageZipParameters); @@ -95,7 +95,7 @@ private ZipParameters createZipParameters(String fileName, char[] password) { return zipParameters; } - private void writeCSV(SQLiteDatabase database, OutputStreamWriter output) throws IOException, InterruptedException { + private void writeCSV(Context context, SQLiteDatabase database, OutputStreamWriter output) throws IOException, InterruptedException { CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180); // Print the version @@ -142,7 +142,7 @@ private void writeCSV(SQLiteDatabase database, OutputStreamWriter output) throws Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database); while (cardCursor.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(cardCursor); + LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor); printer.printRecord(card.id, card.store, @@ -176,7 +176,7 @@ private void writeCSV(SQLiteDatabase database, OutputStreamWriter output) throws Cursor cardCursor2 = DBHelper.getLoyaltyCardCursor(database); while (cardCursor2.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(cardCursor2); + LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor2); for (Group group : DBHelper.getLoyaltyCardGroups(database, card.id)) { printer.printRecord(card.id, group._id); diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index cca491f2d4..08e3f97a74 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -124,7 +124,7 @@ public Map saveAndDeduplicate(Context context, SQLiteDatabase Set existingImages = DBHelper.imageFiles(context, database); for (LoyaltyCard card : data.cards) { - LoyaltyCard existing = DBHelper.getLoyaltyCard(database, card.id); + LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id); if (existing == null) { DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType, card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus); @@ -490,7 +490,26 @@ private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException { // We catch this exception so we can still import old backups } - return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus); + return new LoyaltyCard( + id, + store, + note, + validFrom, + expiry, + balance, + balanceType, + cardId, + barcodeId, + barcodeType, + headerColor, + starStatus, + lastUsed, + DBHelper.DEFAULT_ZOOM_LEVEL, + archiveStatus, + null, + null, + null + ); } /** 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 9275ff2035..e25a41d16b 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -149,7 +149,26 @@ private LoyaltyCard importLoyaltyCard(Context context, CSVRecord record) throws // TODO: Front and back image // use -1 for the ID, it will be ignored when inserting the card into the DB - return new LoyaltyCard(-1, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus); + return new LoyaltyCard( + -1, + store, + note, + null, + null, + BigDecimal.valueOf(0), + null, + cardId, + null, + barcodeType, + headerColor, + starStatus, + Utils.getUnixTime(), + DBHelper.DEFAULT_ZOOM_LEVEL, + archiveStatus, + null, + null, + null + ); } public void saveAndDeduplicate(SQLiteDatabase database, final ImportedData data) { diff --git a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java index 94aa98aa24..c022696bbb 100644 --- a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java @@ -354,7 +354,26 @@ public ImportedData importLoyaltyCardHashMap(Context context, final ZIPData zipD long lastUsed = record.lastUsed != null ? record.lastUsed : Utils.getUnixTime(); - LoyaltyCard card = new LoyaltyCard(tempID, store, note, null, null, BigDecimal.valueOf(0), null, record.cardId, null, barcodeType, headerColor, 0, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, 0); + LoyaltyCard card = new LoyaltyCard( + tempID, + store, + note, + null, + null, + BigDecimal.valueOf(0), + null, + record.cardId, + null, + barcodeType, + headerColor, + 0, + lastUsed, + DBHelper.DEFAULT_ZOOM_LEVEL, + 0, + null, + null, + null + ); importedData.cards.add(card); Map images = new HashMap<>(); 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 b45d81efe3..94c7ba5e8d 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -151,7 +151,26 @@ public ImportedData importJSON(JSONArray jsonArray) throws FormatException, JSON } // use -1 for the ID, it will be ignored when inserting the card into the DB - importedData.cards.add(new LoyaltyCard(-1, store, "", null, expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, 0)); + importedData.cards.add(new LoyaltyCard( + -1, + store, + "", + null, + expiry, + balance, + balanceType, + cardId, + null, + barcodeType, + headerColor, + 0, + Utils.getUnixTime(), + DBHelper.DEFAULT_ZOOM_LEVEL, + 0, + null, + null, + null + )); } return importedData; diff --git a/app/src/main/res/drawable/local_activity_24px.xml b/app/src/main/res/drawable/local_activity_24px.xml new file mode 100644 index 0000000000..21c31ee003 --- /dev/null +++ b/app/src/main/res/drawable/local_activity_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b7b08e16f..881eccf1f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,4 +360,6 @@ Export cancelled Use front image Use back image + Select a Passbook file (.pkpass) + This file is not supported diff --git a/app/src/test/java/protect/card_locker/DatabaseTest.java b/app/src/test/java/protect/card_locker/DatabaseTest.java index a57b02b048..a0e965c8f3 100644 --- a/app/src/test/java/protect/card_locker/DatabaseTest.java +++ b/app/src/test/java/protect/card_locker/DatabaseTest.java @@ -46,7 +46,7 @@ public void addRemoveOneGiftCard() { assertTrue(result); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1); assertNotNull(loyaltyCard); assertEquals("store", loyaltyCard.store); assertEquals("note", loyaltyCard.note); @@ -64,7 +64,7 @@ public void addRemoveOneGiftCard() { result = DBHelper.deleteLoyaltyCard(mDatabase, mActivity, 1); assertTrue(result); assertEquals(0, DBHelper.getLoyaltyCardCount(mDatabase)); - assertNull(DBHelper.getLoyaltyCard(mDatabase, 1)); + assertNull(DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1)); } @Test @@ -78,7 +78,7 @@ public void updateGiftCard() { assertTrue(result); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1); assertNotNull(loyaltyCard); assertEquals("store1", loyaltyCard.store); assertEquals("note1", loyaltyCard.note); @@ -105,7 +105,7 @@ public void updateGiftCardOnlyStar() { assertTrue(result); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1); assertNotNull(loyaltyCard); assertEquals("store", loyaltyCard.store); assertEquals("note", loyaltyCard.note); @@ -138,7 +138,7 @@ public void emptyGiftCardValues() { assertTrue(result); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1); assertNotNull(loyaltyCard); assertEquals("", loyaltyCard.store); assertEquals("", loyaltyCard.note); @@ -480,7 +480,7 @@ public void databaseUpgradeFromVersion1() { dbHelper.onUpgrade(database, DBHelper.ORIGINAL_DATABASE_VERSION, DBHelper.DATABASE_VERSION); // Determine that the entries are queryable and the fields are correct - LoyaltyCard card = DBHelper.getLoyaltyCard(database, newCardId); + LoyaltyCard card = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), database, newCardId); assertEquals("store", card.store); assertEquals("", card.note); assertEquals(null, card.validFrom); @@ -496,7 +496,7 @@ public void databaseUpgradeFromVersion1() { assertEquals(100, card.zoomLevel); // Determine that the entries are queryable and the fields are correct - LoyaltyCard card2 = DBHelper.getLoyaltyCard(database, newCardId2); + LoyaltyCard card2 = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), database, newCardId2); assertEquals("store", card2.store); assertEquals("", card2.note); assertEquals(null, card2.validFrom); @@ -523,7 +523,7 @@ public void updateGiftCardOnlyBalance() { assertTrue(result); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(mActivity.getApplicationContext(), mDatabase, 1); assertNotNull(loyaltyCard); assertEquals("store", loyaltyCard.store); assertEquals("note", loyaltyCard.note); diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index c90bff3db5..13bf3eb4a2 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -94,7 +94,7 @@ public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { boolean result = (id != -1); assertTrue(result); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, (int) id); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, (int) id); assertEquals("No Expiry", card.store); assertEquals("", card.note); assertEquals(null, card.validFrom); @@ -111,7 +111,7 @@ public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { result = (id != -1); assertTrue(result); - card = DBHelper.getLoyaltyCard(mDatabase, (int) id); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, (int) id); assertEquals("Past", card.store); assertEquals("", card.note); assertEquals(null, card.validFrom); @@ -128,7 +128,7 @@ public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { result = (id != -1); assertTrue(result); - card = DBHelper.getLoyaltyCard(mDatabase, (int) id); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, (int) id); assertEquals("Today", card.store); assertEquals("", card.note); assertEquals(null, card.validFrom); @@ -148,7 +148,7 @@ public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { result = (id != -1); assertTrue(result); - card = DBHelper.getLoyaltyCard(mDatabase, (int) id); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, (int) id); assertEquals("Future", card.store); assertEquals("", card.note); assertEquals(null, card.validFrom); @@ -174,7 +174,7 @@ private void checkLoyaltyCards() { int index = 1; while (cursor.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(cursor); + LoyaltyCard card = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); String expectedStore = String.format("store, \"%4d", index); String expectedNote = String.format("note, \"%4d", index); @@ -200,7 +200,7 @@ private void checkLoyaltyCardsAndDuplicates(int numCards) { Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); while (cursor.moveToNext()) { - LoyaltyCard card = LoyaltyCard.fromCursor(cursor); + LoyaltyCard card = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); // ID goes up for duplicates (b/c the cursor orders by store), down for originals int index = card.id > numCards ? card.id - numCards : numCards - card.id + 1; @@ -236,7 +236,7 @@ private void checkLoyaltyCardsFiveStarred() { while (index < 10) { cursor.moveToNext(); - LoyaltyCard card = LoyaltyCard.fromCursor(cursor); + LoyaltyCard card = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); String expectedStore = String.format("store, \"%4d", index); String expectedNote = String.format("note, \"%4d", index); @@ -258,7 +258,7 @@ private void checkLoyaltyCardsFiveStarred() { index = 1; while (cursor.moveToNext() && index < 5) { - LoyaltyCard card = LoyaltyCard.fromCursor(cursor); + LoyaltyCard card = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); String expectedStore = String.format("store, \"%4d", index); String expectedNote = String.format("note, \"%4d", index); @@ -649,7 +649,7 @@ public void importWithoutColorsV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -675,7 +675,7 @@ public void importWithoutNullColorsV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -713,7 +713,7 @@ public void importWithNoBarcodeTypeV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -739,7 +739,7 @@ public void importWithStarredFieldV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -765,7 +765,7 @@ public void importWithNoStarredFieldV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -798,7 +798,7 @@ public void importWithInvalidStarFieldV1() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(1, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("store", card.store); assertEquals("note", card.note); @@ -831,7 +831,7 @@ public void exportImportV2Zip() throws FileNotFoundException { // Create card 1 int loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, "Card 1", "Note 1", new Date(1601510400), new Date(1618053234), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), 1, 0, null,0); - loyaltyCardHashMap.put(loyaltyCardId, DBHelper.getLoyaltyCard(mDatabase, loyaltyCardId)); + loyaltyCardHashMap.put(loyaltyCardId, DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, loyaltyCardId)); DBHelper.insertGroup(mDatabase, "One"); List groups = Arrays.asList(DBHelper.getGroup(mDatabase, "One")); DBHelper.setLoyaltyCardGroups(mDatabase, loyaltyCardId, groups); @@ -845,7 +845,7 @@ public void exportImportV2Zip() throws FileNotFoundException { // Create card 2 loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, "Card 2", "", null, null, new BigDecimal(0), null, "123456", null, null, 2, 1, null,0); - loyaltyCardHashMap.put(loyaltyCardId, DBHelper.getLoyaltyCard(mDatabase, loyaltyCardId)); + loyaltyCardHashMap.put(loyaltyCardId, DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, loyaltyCardId)); // Export everything ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -863,7 +863,7 @@ public void exportImportV2Zip() throws FileNotFoundException { for (Integer loyaltyCardID : loyaltyCardHashMap.keySet()) { LoyaltyCard loyaltyCard = loyaltyCardHashMap.get(loyaltyCardID); - LoyaltyCard dbLoyaltyCard = DBHelper.getLoyaltyCard(mDatabase, loyaltyCardID); + LoyaltyCard dbLoyaltyCard = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, loyaltyCardID); assertEquals(loyaltyCard.id, dbLoyaltyCard.id); assertEquals(loyaltyCard.store, dbLoyaltyCard.store); @@ -950,7 +950,7 @@ public void importV2CSV() { assertEquals(Arrays.asList(8, 6), DBHelper.getGroupCardIds(mDatabase, "Fashion")); // Check all cards - LoyaltyCard card1 = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card1 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("Card 1", card1.store); assertEquals("Note 1", card1.note); @@ -967,7 +967,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, ImageLocationType.icon)); - LoyaltyCard card8 = DBHelper.getLoyaltyCard(mDatabase, 8); + LoyaltyCard card8 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 8); assertEquals("Clothes Store", card8.store); assertEquals("Note about store", card8.note); @@ -984,7 +984,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, ImageLocationType.icon)); - LoyaltyCard card2 = DBHelper.getLoyaltyCard(mDatabase, 2); + LoyaltyCard card2 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 2); assertEquals("Department Store", card2.store); assertEquals("", card2.note); @@ -1001,7 +1001,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, ImageLocationType.icon)); - LoyaltyCard card3 = DBHelper.getLoyaltyCard(mDatabase, 3); + LoyaltyCard card3 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 3); assertEquals("Grocery Store", card3.store); assertEquals("Multiline note about grocery store\n\nwith blank line", card3.note); @@ -1018,7 +1018,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, ImageLocationType.icon)); - LoyaltyCard card4 = DBHelper.getLoyaltyCard(mDatabase, 4); + LoyaltyCard card4 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 4); assertEquals("Pharmacy", card4.store); assertEquals("", card4.note); @@ -1035,7 +1035,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, ImageLocationType.icon)); - LoyaltyCard card5 = DBHelper.getLoyaltyCard(mDatabase, 5); + LoyaltyCard card5 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 5); assertEquals("Restaurant", card5.store); assertEquals("Note about restaurant here", card5.note); @@ -1052,7 +1052,7 @@ public void importV2CSV() { assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, ImageLocationType.back)); assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, ImageLocationType.icon)); - LoyaltyCard card6 = DBHelper.getLoyaltyCard(mDatabase, 6); + LoyaltyCard card6 = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 6); assertEquals("Shoe Store", card6.store); assertEquals("", card6.note); @@ -1081,7 +1081,7 @@ public void importFidme() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(3, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("Hema", card.store); assertEquals("2021-03-24 18:35:08 UTC", card.note); @@ -1094,7 +1094,7 @@ public void importFidme() { assertEquals(null, card.barcodeType); assertEquals(0, card.starStatus); - card = DBHelper.getLoyaltyCard(mDatabase, 2); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 2); assertEquals("test", card.store); assertEquals("Test\n2021-03-24 18:34:19 UTC", card.note); @@ -1107,7 +1107,7 @@ public void importFidme() { assertEquals(null, card.barcodeType); assertEquals(0, card.starStatus); - card = DBHelper.getLoyaltyCard(mDatabase, 3); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 3); assertEquals("Albert Heijn", card.store); assertEquals("Bonus Kaart\n2021-03-24 16:47:47 UTC\nFirst Last", card.note); @@ -1138,7 +1138,7 @@ public void importStocard() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(3, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("Air Miles", card.store); assertEquals("szjsbs", card.note); @@ -1156,7 +1156,7 @@ public void importStocard() { assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-back.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.back))); assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.icon)); - card = DBHelper.getLoyaltyCard(mDatabase, 2); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 2); assertEquals("GAMMA", card.store); assertEquals("", card.note); @@ -1174,7 +1174,7 @@ public void importStocard() { assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.back)); assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.icon)); - card = DBHelper.getLoyaltyCard(mDatabase, 3); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 3); assertEquals("jö", card.store); assertEquals("", card.note); @@ -1205,7 +1205,7 @@ public void importStocard2() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(4, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("Foo", card.store); assertEquals("", card.note); @@ -1223,7 +1223,7 @@ public void importStocard2() { assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.back)); assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, ImageLocationType.icon)); - card = DBHelper.getLoyaltyCard(mDatabase, 2); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 2); assertEquals("Air Miles", card.store); assertEquals("szjsbs\nMiles", card.note); @@ -1241,7 +1241,7 @@ public void importStocard2() { assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-back.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.back))); assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 2, ImageLocationType.icon)); - card = DBHelper.getLoyaltyCard(mDatabase, 3); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 3); assertEquals("GAMMA", card.store); assertEquals("", card.note); @@ -1259,7 +1259,7 @@ public void importStocard2() { assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, ImageLocationType.back)); assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, ImageLocationType.icon)); - card = DBHelper.getLoyaltyCard(mDatabase, 4); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 4); assertEquals("jö", card.store); assertEquals("", card.note); @@ -1289,7 +1289,7 @@ public void importVoucherVault() { assertEquals(ImportExportResultType.Success, result.resultType()); assertEquals(2, DBHelper.getLoyaltyCardCount(mDatabase)); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); assertEquals("Clothes Store", card.store); assertEquals("", card.note); @@ -1303,7 +1303,7 @@ public void importVoucherVault() { assertEquals(Color.GRAY, (long) card.headerColor); assertEquals(0, card.starStatus); - card = DBHelper.getLoyaltyCard(mDatabase, 2); + card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 2); assertEquals("Department Store", card.store); assertEquals("", card.note); diff --git a/app/src/test/java/protect/card_locker/ImportURITest.java b/app/src/test/java/protect/card_locker/ImportURITest.java index 492c6ae3e2..f0b8678271 100644 --- a/app/src/test/java/protect/card_locker/ImportURITest.java +++ b/app/src/test/java/protect/card_locker/ImportURITest.java @@ -25,12 +25,13 @@ @RunWith(RobolectricTestRunner.class) public class ImportURITest { + private Activity activity; private ImportURIHelper importURIHelper; private SQLiteDatabase mDatabase; @Before public void setUp() { - Activity activity = Robolectric.setupActivity(MainActivity.class); + activity = Robolectric.setupActivity(MainActivity.class); importURIHelper = new ImportURIHelper(activity); mDatabase = TestHelpers.getEmptyDb(activity).getWritableDatabase(); } @@ -43,7 +44,7 @@ public void ensureNoDataLoss() throws InvalidObjectException, UnsupportedEncodin DBHelper.insertLoyaltyCard(mDatabase, "store", "This note contains evil symbols like & and = that will break the parser if not escaped right $#!%()*+;:á", date, date, new BigDecimal("100"), null, BarcodeFormat.UPC_E.toString(), BarcodeFormat.UPC_A.toString(), CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), Color.BLACK, 1, null,0); // Get card - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); // Card to URI Uri cardUri = importURIHelper.toUri(card); @@ -73,7 +74,7 @@ public void ensureNoCrashOnMissingHeaderFields() throws InvalidObjectException, DBHelper.insertLoyaltyCard(mDatabase, "store", "note", null, null, new BigDecimal("10.00"), Currency.getInstance("EUR"), BarcodeFormat.UPC_A.toString(), null, CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), null, 0, null,0); // Get card - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); // Card to URI Uri cardUri = importURIHelper.toUri(card); @@ -103,7 +104,7 @@ public void parseWithTrailingSlash() throws InvalidObjectException, UnsupportedE DBHelper.insertLoyaltyCard(mDatabase, "store", "note", null, null, new BigDecimal("10.00"), Currency.getInstance("EUR"), BarcodeFormat.UPC_A.toString(), null, CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE), null, 0, null,0); // Get card - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); // Card to URI, with a trailing slash Uri cardUri = importURIHelper.toUri(card).buildUpon().path("/share/").build(); diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java index 1ada6674d5..7f647c1614 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java @@ -97,7 +97,7 @@ private void checkView(final View view, final String store, final String note, f @Test public void TestCursorAdapterEmptyNote() { DBHelper.insertLoyaltyCard(mDatabase, "store", "", null, null, new BigDecimal("0"), null, "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); @@ -112,7 +112,7 @@ public void TestCursorAdapterEmptyNote() { @Test public void TestCursorAdapterWithNote() { DBHelper.insertLoyaltyCard(mDatabase, "store", "note", null, null, new BigDecimal("0"), null, "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); @@ -137,7 +137,7 @@ public void TestCursorAdapterStarring() { assertEquals(4, cursor.getCount()); assertTrue(cursor.moveToFirst()); - LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(cursor); + LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); assertEquals("storeD", loyaltyCard.store); View view = createView(cursor); ConstraintLayout star = view.findViewById(R.id.star); @@ -146,7 +146,7 @@ public void TestCursorAdapterStarring() { assertEquals(View.GONE, archive.getVisibility()); assertTrue(cursor.moveToNext()); - loyaltyCard = LoyaltyCard.fromCursor(cursor); + loyaltyCard = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); assertEquals("storeC", loyaltyCard.store); view = createView(cursor); star = view.findViewById(R.id.star); @@ -155,7 +155,7 @@ public void TestCursorAdapterStarring() { assertEquals(View.GONE, archive.getVisibility()); assertTrue(cursor.moveToNext()); - loyaltyCard = LoyaltyCard.fromCursor(cursor); + loyaltyCard = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); assertEquals("storeB", loyaltyCard.store); view = createView(cursor); star = view.findViewById(R.id.star); @@ -164,7 +164,7 @@ public void TestCursorAdapterStarring() { assertEquals(View.VISIBLE, archive.getVisibility()); assertTrue(cursor.moveToNext()); - loyaltyCard = LoyaltyCard.fromCursor(cursor); + loyaltyCard = LoyaltyCard.fromCursor(activity.getApplicationContext(), cursor); assertEquals("storeA", loyaltyCard.store); view = createView(cursor); star = view.findViewById(R.id.star); @@ -178,7 +178,7 @@ public void TestCursorAdapterStarring() { @Test public void TestCursorAdapter0Points() { DBHelper.insertLoyaltyCard(mDatabase, "store", "", null, null, new BigDecimal("0"), null, "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); @@ -193,7 +193,7 @@ public void TestCursorAdapter0Points() { @Test public void TestCursorAdapter0EUR() { DBHelper.insertLoyaltyCard(mDatabase,"store", "", null, null, new BigDecimal("0"), Currency.getInstance("EUR"), "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); @@ -208,7 +208,7 @@ public void TestCursorAdapter0EUR() { @Test public void TestCursorAdapter100Points() { DBHelper.insertLoyaltyCard(mDatabase, "store", "note", null, null, new BigDecimal("100"), null, "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); @@ -223,7 +223,7 @@ public void TestCursorAdapter100Points() { @Test public void TestCursorAdapter10USD() { DBHelper.insertLoyaltyCard(mDatabase, "store", "note", null, null, new BigDecimal("10.00"), Currency.getInstance("USD"), "cardId", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), Color.BLACK, 0, null,0); - LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), mDatabase, 1); Cursor cursor = DBHelper.getLoyaltyCardCursor(mDatabase); cursor.moveToFirst(); diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index eb61e0316c..f93015c6ae 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -167,7 +167,7 @@ private void saveLoyaltyCardWithArguments(final Activity activity, assertEquals(1, DBHelper.getLoyaltyCardCount(database)); - LoyaltyCard card = DBHelper.getLoyaltyCard(database, 1); + LoyaltyCard card = DBHelper.getLoyaltyCard(activity.getApplicationContext(), database, 1); assertEquals(store, card.store); assertEquals(note, card.note); assertEquals(balance, card.balance); @@ -231,10 +231,14 @@ private void captureBarcodeWithResult(final Activity activity, final boolean suc assertNotNull(bundle); Intent resultIntent = new Intent(intent); - Bundle resultBundle = new Bundle(); - resultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, BARCODE_DATA); - resultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, BARCODE_TYPE.name()); - resultIntent.putExtras(resultBundle); + + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setBarcodeId(null); + loyaltyCard.setBarcodeType(BARCODE_TYPE); + loyaltyCard.setCardId(BARCODE_DATA); + ParseResult parseResult = new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard); + + resultIntent.putExtras(parseResult.toLoyaltyCardBundle()); // Respond to image capture, success shadowOf(activity).receiveResult( @@ -267,10 +271,14 @@ private void selectBarcodeWithResult(final Activity activity, final String barco assertNotNull(bundle); Intent resultIntent = new Intent(intent); - Bundle resultBundle = new Bundle(); - resultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, barcodeType); - resultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, barcodeData); - resultIntent.putExtras(resultBundle); + + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setBarcodeId(null); + loyaltyCard.setBarcodeType(barcodeType != null ? CatimaBarcode.fromName(barcodeType) : null); + loyaltyCard.setCardId(barcodeData); + ParseResult parseResult = new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard); + + resultIntent.putExtras(parseResult.toLoyaltyCardBundle()); // Respond to barcode selection, success shadowOf(activity).receiveResult( diff --git a/app/src/test/java/protect/card_locker/PkpassTest.kt b/app/src/test/java/protect/card_locker/PkpassTest.kt new file mode 100644 index 0000000000..242e046a10 --- /dev/null +++ b/app/src/test/java/protect/card_locker/PkpassTest.kt @@ -0,0 +1,238 @@ +package protect.card_locker + +import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.Color +import android.net.Uri +import androidx.test.core.app.ApplicationProvider +import com.google.zxing.BarcodeFormat +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowContentResolver +import org.robolectric.shadows.ShadowLog +import java.math.BigDecimal +import java.util.Date + +@RunWith(RobolectricTestRunner::class) +class PkpassTest { + @Before + fun setUp() { + ShadowLog.stream = System.out + } + + @Test + fun testEurowingsPass() { + // Prepare + val context: Context = ApplicationProvider.getApplicationContext() + val pkpass = "pkpass/Eurowings/Eurowings.pkpass" + val image = "pkpass/Eurowings/logo@2x.png" + + val pkpassUri = Uri.parse(pkpass) + val imageUri = Uri.parse(image) + ShadowContentResolver().registerInputStream(pkpassUri, javaClass.getResourceAsStream(pkpass)) + ShadowContentResolver().registerInputStream(imageUri, javaClass.getResourceAsStream(image)) + + val parser = PkpassParser(context, pkpassUri) + val imageBitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(imageUri)) + + // Confirm this does not have languages + Assert.assertEquals(listOf("de", "en"), parser.listLocales()) + + // Confirm correct parsing (en) + var parsedCard = parser.toLoyaltyCard("de") + + Assert.assertEquals(-1, parsedCard.id) + Assert.assertEquals("EUROWINGS", parsedCard.store) + Assert.assertEquals("Eurowings Boarding Pass\n" + + "\n" + + "Gate: B61\n" + + "Sitz: 12D\n" + + "\n" + + "Cologne-Bonn: CGN\n" + + "Dubrovnik: DBV\n" + + "\n" + + "Name: John Doe\n" + + "Status: -\n" + + "Gruppe: GROUP 1\n" + + "Tarif: SMART\n" + + "\n" + + "Flug: EW 954\n" + + "Datum: 08/09/2019\n" + + "Boarding: 05:00\n" + + "Gate Schließt: 05:15\n" + + "\n" + + "Eurowings wünscht Ihnen einen angenehmen Flug.\n" + + "\n" + + "Wir bitten Sie, sich zur angegeben Boarding Zeit am Gate einzufinden.\n" + + "Buchungscode: JBZPPP\n" + + "Sequenz: 73\n" + + "Hinweis: Bitte beachten Sie, dass obwohl Ihr Flug verspätet sein mag, Sie dennoch wie geplant pünktlich am Check-in und am Abfluggate erscheinen müssen.\n" + + "\n" + + "Kostenlose Mitnahme eines Handgepäckstücks (8 Kg, 55 x 40 x 23cm).\n" + + "Mitnahme von Flüssigkeiten im Handgepäck: Neben den sonstigen Beschränkungen für das Handgepäck ist für alle Abflüge innerhalb der Europäischen Union sowie vielen weiteren Ländern (u.a. Schweiz, Russland, Island, Kroatien, Israel, Ägypten, Marokko, Tunesien, Norwegen) die Mitnahme von vor der Fluggastkontrolle erworbenen bzw. mitgebrachten Flüssigkeiten und Gels nur noch eingeschränkt erlaubt:\n" + + "\n" + + "- Sämtliche Flüssigkeiten (wie Kosmetik- und Toilettenartikel, Gels, Pasten, Cremes, Lotionen, Gemische aus flüssigen und festen Stoffen, Parfums, Behälter unter Druck, Dosen, Wasserflaschen etc.) sowie wachs- oder gelartige Stoffe dürfen nur noch in Behältnissen bis zu 100 ml bzw. 100 g mit an Bord genommen werden.\n" + + "\n" + + "- Diese Flüssigkeiten bzw. Stoffe müssen in einem transparenten, wiederverschließbaren Plastikbeutel (max. 1 kg Inhalt) vollständig geschlossen, verpackt sein.\n" + + "\n" + + "- Diese Beutel müssen Fluggäste selbst vor dem Abflug erwerben. Sie sind in vielen Supermärkten z. B. als Gefrierbeutel erhältlich. Es besteht zurzeit keine Möglichkeit, entsprechende Plastikbeutel am Eurowings Check-In zu erwerben bzw. auszugeben.\n" + + "\n" + + "- Verschreibungspflichtige Medikamente sowie Babynahrung dürfen weiterhin im Handgepäck transportiert werden. Der Fluggast muss nachweisen, dass die Medikamente und Babynahrung während des Fluges benötigt werden.\n" + + "\n" + + "- Produkte und Beutel, die nicht den Maßgaben entsprechen oder die nur mit Gummiband oder ähnlichem verschlossen sind, müssen leider abgegeben werden.\n" + + "\n" + + "Flüssigkeiten und Gels, die Sie nicht zwingend während Ihres Aufenthalts an Bord benötigen, sollten zur raschen Fluggastabfertigung nach Möglichkeit im aufzugebenden Gepäck untergebracht werden.\n" + + "\n" + + "Selbstverständlich ist die Mitnahme von allen Flüssigkeiten/Gels/Getränken aus Travel-Value oder Duty Free-Shops, die nach der Fluggastkontrolle erworben werden, weiterhin erlaubt.\n" + + "\n" + + "Eurowings übernimmt keine Haftung für Gegenstände, die der Fluggast nicht im Handgepäck mitführen darf und deshalb aus Sicherheitsgründen an der Fluggastkontrolle abgeben muss.\n" + + "Kontakt: Sie erreichen das deutsche Call Center unter der Telefonnummer\n" + + "\n" + + "0180 6 320 320 ( 0:00 Uhr - 24:00 Uhr )\n" + + "\n" + + "(0,20 € pro Anruf aus dem Festnetz der Deutschen Telekom - Mobilfunk maximal 0,60 € pro Anruf).", parsedCard.note) + Assert.assertEquals(Date(1567911600000), parsedCard.validFrom) + Assert.assertEquals(null, parsedCard.expiry) + Assert.assertEquals(BigDecimal(0), parsedCard.balance) + Assert.assertEquals(null, parsedCard.balanceType) + Assert.assertEquals("M1DOE/JOHN JBZPPP CGNDBVEW 0954 251A012D0073 148>5181W 9250BEW 00000000000002A0000000000000 0 N", parsedCard.cardId) + Assert.assertEquals(null, parsedCard.barcodeId) + Assert.assertEquals(BarcodeFormat.AZTEC, parsedCard.barcodeType!!.format()) + Assert.assertEquals(Color.parseColor("#FFFFFF"), parsedCard.headerColor) + Assert.assertEquals(0, parsedCard.starStatus) + Assert.assertEquals(0, parsedCard.archiveStatus) + Assert.assertEquals(0, parsedCard.lastUsed) + Assert.assertEquals(DBHelper.DEFAULT_ZOOM_LEVEL, parsedCard.zoomLevel) + + // Confirm correct image is used + Assert.assertTrue(imageBitmap.sameAs(parser.image)) + + // Confirm correct parsing (en) + parsedCard = parser.toLoyaltyCard("en") + + Assert.assertEquals(-1, parsedCard.id) + Assert.assertEquals("EUROWINGS", parsedCard.store) + Assert.assertEquals("Eurowings Boarding Pass\n" + + "\n" + + "Gate: B61\n" + + "Seat: 12D\n" + + "\n" + + "Cologne-Bonn: CGN\n" + + "Dubrovnik: DBV\n" + + "\n" + + "Name: John Doe\n" + + "Status: -\n" + + "Group: GROUP 1\n" + + "Fare: SMART\n" + + "\n" + + "Flight: EW 954\n" + + "Date: 08/09/2019\n" + + "Boarding: 05:00\n" + + "Gate closure: 05:15\n" + + "\n" + + "Eurowings wishes you a pleasant flight .\n" + + "\n" + + "We kindly ask you to be present at your departure gate on time.\n" + + "Booking code: JBZPPP\n" + + "Sequence: 73\n" + + "Notice: Please note that although your flight may be delayed, you will still need to check in and go to your departure gate on time as scheduled.\n" + + "\n" + + "Carry on one item of hand luggage (8 kg, 55 x 40 x 23 cm) for free.\n" + + "Carrying liquids in hand luggage: In addition to other restrictions on hand luggage, there are still restrictions on liquids and gels brought by the passenger or purchased before the security control on all departures within the European Union, as well as to many other countries (including Switzerland, Russia, Iceland, Croatia, Israel, Egypt, Morocco, Tunisia and Norway):\n" + + "\n" + + "- All liquids (such as toiletries and cosmetics, gels, pastes, creams, lotions, mixtures of liquids and solids, perfumes, pressurised containers, cans, water bottles etc) as well as wax and gel-like substances may only be carried on board in amounts less than 100ml or 100g.\n" + + "\n" + + "- These liquids or substances must be packed in closed containers in a transparent, re-sealable plastic bag (max. contents 1 kg).\n" + + "\n" + + "- It is the passenger’s responsibility to purchase this bag before departure. They are available in many supermarkets, e.g. as freezer bags. It is currently not possible for passengers to obtain or purchase the required bags from Eurowings check-in.\n" + + "\n" + + "- Prescription medicines and baby food may still be carried in hand baggage. The passenger must prove that such medicines and/or baby food are needed during the flight.\n" + + "\n" + + "- Products and bags which do not meet the requirements or are only sealed with a rubber band or similar will unfortunately have to be surrendered by passengers\n" + + "\n" + + "In order to pass through the airport as quickly as possible, you are strongly advised to pack any liquids or gels which are not essential for your journey on board the aircraft in your checked baggage if possible.\n" + + "\n" + + "As a matter of course, liquids from the Travel Value / Duty Free shops which have been purchased after you have passed through security are still allowed on board.\n" + + "\n" + + "Eurowings shall not be liable for any items which passengers are prohibited from carrying in their hand baggage for security reasons and are required to surrender at the security checkpoint.\n" + + "Contact: https://mobile.eurowings.com/booking/StaticContactInfo.aspx?culture=en-GB&back=home", parsedCard.note) + Assert.assertEquals(Date(1567911600000), parsedCard.validFrom) + Assert.assertEquals(null, parsedCard.expiry) + Assert.assertEquals(BigDecimal(0), parsedCard.balance) + Assert.assertEquals(null, parsedCard.balanceType) + Assert.assertEquals("M1DOE/JOHN JBZPPP CGNDBVEW 0954 251A012D0073 148>5181W 9250BEW 00000000000002A0000000000000 0 N", parsedCard.cardId) + Assert.assertEquals(null, parsedCard.barcodeId) + Assert.assertEquals(BarcodeFormat.AZTEC, parsedCard.barcodeType!!.format()) + Assert.assertEquals(Color.parseColor("#FFFFFF"), parsedCard.headerColor) + Assert.assertEquals(0, parsedCard.starStatus) + Assert.assertEquals(0, parsedCard.archiveStatus) + Assert.assertEquals(0, parsedCard.lastUsed) + Assert.assertEquals(DBHelper.DEFAULT_ZOOM_LEVEL, parsedCard.zoomLevel) + + // Confirm correct image is used + Assert.assertTrue(imageBitmap.sameAs(parser.image)) + } + + @Test + fun testDCBPkPass() { + // Prepare + val context: Context = ApplicationProvider.getApplicationContext() + val pkpass = "pkpass/DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass" + val image = "pkpass/DCBLN24/logo.png" + + val pkpassUri = Uri.parse(pkpass) + val imageUri = Uri.parse(image) + ShadowContentResolver().registerInputStream(pkpassUri, javaClass.getResourceAsStream(pkpass)) + ShadowContentResolver().registerInputStream(imageUri, javaClass.getResourceAsStream(image)) + + val parser = PkpassParser(context, pkpassUri) + val imageBitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(imageUri)) + + // Confirm this does not have languages + Assert.assertEquals(listOf(), parser.listLocales()) + + // Confirm correct parsing + val parsedCard = parser.toLoyaltyCard(null) + + Assert.assertEquals(-1, parsedCard.id) + Assert.assertEquals("droidcon Berlin 2024", parsedCard.store) + Assert.assertEquals("Ticket for droidcon Berlin 2024 (Speaker)\n" + + "\n" + + "Admission time: 2024-07-03 08:00\n" + + "\n" + + "Event: droidcon Berlin 2024\n" + + "\n" + + "Product: Speaker\n" + + "\n" + + "Attendee name: Sylvia van Os\n" + + "From: 2024-07-03 08:00\n" + + "To: 2024-07-05 18:30\n" + + "\n" + + "Admission time: 2024-07-03 08:00\n" + + "Attendee name: Sylvia van Os\n" + + "Ordered by: REDACTED@example.com\n" + + "Organizer: droidcon\n" + + "Organizer contact: global@droidcon.de\n" + + "Order code: REDACTED\n" + + "Purchase date: 2024-06-06 07:26\n" + + "Website: https://pretix.eu/droidcon/dcbln24/", parsedCard.note) + Assert.assertEquals(null, parsedCard.validFrom) + Assert.assertEquals(null, parsedCard.expiry) + Assert.assertEquals(BigDecimal(0), parsedCard.balance) + Assert.assertEquals(null, parsedCard.balanceType) + Assert.assertEquals("ca4phaix1ahkahD2eiVi5iepahxa6rei", parsedCard.cardId) + Assert.assertEquals(null, parsedCard.barcodeId) + Assert.assertEquals(BarcodeFormat.QR_CODE, parsedCard.barcodeType!!.format()) + Assert.assertEquals(Color.parseColor("#0014e6"), parsedCard.headerColor) + Assert.assertEquals(0, parsedCard.starStatus) + Assert.assertEquals(0, parsedCard.archiveStatus) + Assert.assertEquals(0, parsedCard.lastUsed) + Assert.assertEquals(DBHelper.DEFAULT_ZOOM_LEVEL, parsedCard.zoomLevel) + + // Confirm correct image is used + Assert.assertTrue(imageBitmap.sameAs(parser.image)) + } +} diff --git a/app/src/test/res/protect/card_locker/pkpass/DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass b/app/src/test/res/protect/card_locker/pkpass/DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass new file mode 100644 index 0000000000..b6d4b08f5c Binary files /dev/null and b/app/src/test/res/protect/card_locker/pkpass/DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass differ diff --git a/app/src/test/res/protect/card_locker/pkpass/DCBLN24/logo.png b/app/src/test/res/protect/card_locker/pkpass/DCBLN24/logo.png new file mode 100644 index 0000000000..362d1990a5 Binary files /dev/null and b/app/src/test/res/protect/card_locker/pkpass/DCBLN24/logo.png differ diff --git a/app/src/test/res/protect/card_locker/pkpass/Eurowings/Eurowings.pkpass b/app/src/test/res/protect/card_locker/pkpass/Eurowings/Eurowings.pkpass new file mode 100644 index 0000000000..f2265f66e1 Binary files /dev/null and b/app/src/test/res/protect/card_locker/pkpass/Eurowings/Eurowings.pkpass differ diff --git a/app/src/test/res/protect/card_locker/pkpass/Eurowings/logo@2x.png b/app/src/test/res/protect/card_locker/pkpass/Eurowings/logo@2x.png new file mode 100644 index 0000000000..ba1a78ff58 Binary files /dev/null and b/app/src/test/res/protect/card_locker/pkpass/Eurowings/logo@2x.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 9aa5deeb26..ffe335ab7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.application") version "8.7.2" apply false id("com.github.spotbugs") version "5.1.4" apply false + id("org.jetbrains.kotlin.android") version "2.0.0" apply false } allprojects {