-
- Notifications
You must be signed in to change notification settings - Fork 217
Fixed card duplication issue #2349 #2759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fixed card duplication issue #2349 #2759
Conversation
These tests don't seem relevant to this PR. Can you please split that into another PR, to keep this one focused on the change it is supposed to fix? That makes reviewing much easier. |
a0a0020 to 4bf0862 Compare | @TheLastProject I removed tests from this PR. |
| I'm a bit confused, you seem to just be checking if the existing and to-import card both have images and the same ID? That doesn't sound like it fixes images being detected as different when they aren't. Instead, you're now most likely overwriting images which aren't the same, which would be way worse! Unless I'm misreading your change somehow? |
| @TheLastProject here is a video of testing: |
| Sorry for the low video quality, I had to compress it a lot. But I also tested it and I could fairly easily get your change to cause data loss (overriding an existing card image) as I feared it would: Screen_recording_20251015_204655.mp4 |
| @TheLastProject you were right. I looked into it and fixed it. It should work fine now. |
| After looking more deeply at this, I am starting to understand your attempted fix better. I think there is a better solution though. The whole image checksum stuff is from the days before a So I think basically all we need to do is:
That basically would mean throwing the code away though, but it would be a much cleaner fix 😅 I think that also means migrating the string matching to use a Pattern.matcher (https://developer.android.com/reference/java/util/regex/Pattern#matcher(java.lang.CharSequence)) where you can get the card ID and front/back/icon out of the filename |
| I did everything as you wanted. I hope it's ready for merging now. |
| It seems the tests are failing 😅 |
TheLastProject left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested the new code yet, but I think this flow makes sense, this may be mostly correct.
I do think you have a fair bit of dead code currently. A lot of checking if a card exists, when you actually only have 2 states which you already decided earlier:
- Card is the same as a card that already exists (then you just ignore it)
- Card is not the same, so needs to be inserted as a new card
So I feel that after cleaning that up this code may very well be correct and done? Of course, I'll have to test to make very sure though, but the general flow does feel correct now!
| public static List<LoyaltyCard> getLoyaltyCardsByCardId(Context context, SQLiteDatabase database, final String cardId) { | ||
| List<LoyaltyCard> cards = new ArrayList<>(); | ||
| | ||
| Cursor data = database.query( | ||
| LoyaltyCardDbIds.TABLE, | ||
| null, | ||
| whereAttrs(LoyaltyCardDbIds.CARD_ID), | ||
| withArgs(cardId), | ||
| null, | ||
| null, | ||
| null | ||
| ); | ||
| | ||
| if (data.moveToFirst()) { | ||
| do { | ||
| LoyaltyCard card = LoyaltyCard.fromCursor(context, data); | ||
| cards.add(card); | ||
| } while (data.moveToNext()); | ||
| } | ||
| | ||
| data.close(); | ||
| return cards; | ||
| } | ||
| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if you confused this with the unique ID before (this is the barcode value basically). If your plan was to limit the amount of loyalty cards to loop over, you could still just use getLoyaltyCardCursor and limit it by the store instead of creating a new function. The DBHelper class is already way too complex, so I'd rather prevent extra complexity there whenever possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know it's a barcode. I didn't want to compare the imported card with all the others, so I chose the barcode as a filter.
Previously, the code only compared the card with an existing card with the same ID, which led to duplicates. Since the ID is assigned randomly, it must be removed from the comparison.
This part of the code is therefore essential from a performance perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the code is therefore essential from a performance perspective.
I mean, not really, you can also just call one of the existing getLoyaltyCardCursor functions and use the store name as the filter. In fact that would be better, because a cursor lets you loop over the results without loading them all into memory :)
I do agree that removing the database ID from the comparison is correct though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think about a reality.
How many users want to have multiple cards with same Barcode?
How many users want to have multiple cards with same store name?
Both of these are very rare, but I think that the first one is absolutely illogical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reality is that both will be fairly rare, but the second one doesn't need a new function. And thus is cleaner and easier for long-term maintenance of the codebase.
| return false; | ||
| // One is null and the other isn't, so it's not equal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this swapped?
| if (!isZipFile) { | ||
| if (zipFile.isValidZipFile()) { | ||
| importedData = importZIP(zipFile); | ||
| }else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coding style
| }else { | |
| } else { |
| public ImportedData parseV1(BufferedReader input) throws IOException, FormatException, InterruptedException { | ||
| public ImportedData importZIP(ZipFile zipFile) throws IOException, FormatException, InterruptedException { | ||
| FileHeader fileHeader = zipFile.getFileHeader("catima.csv"); | ||
| if (fileHeader == null){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code style
| if (fileHeader == null){ | |
| if (fileHeader == null) { |
| 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); | ||
| } else if (!isDuplicate(context, existing, card, existingImages, imageChecksums)) { | ||
| long newId = DBHelper.insertLoyaltyCard(database, 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); | ||
| idMap.put(card.id, (int) newId); | ||
| | ||
| if (candidates.isEmpty()) { | ||
| if (existing != null && existing.id == card.id) { | ||
| long newId = insertCard(context, database, card, true); | ||
| idMap.put(card.id, (int) newId); | ||
| }else{ | ||
| insertCard(context, database, card, false); | ||
| } | ||
| }else if (!duplicateFound) { | ||
| if (existing != null && existing.id == card.id) { | ||
| long newId = insertCard(context, database, card, true); | ||
| idMap.put(card.id, (int) newId); | ||
| }else{ | ||
| insertCard(context, database, card, false); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a bit weird. You're first checking if the card is a duplicate and after that you check if it matches with an existing card.
Wouldn't it be more logical to just leave this loop if the card is a duplicate, as you don't want to import a duplicate anyway? That would severely simplify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... yes, the outer if is unnecessary. I'll delete it.
The imported card is first compared with potential duplicates. If the card is a duplicate, it is not imported. But if it is not a duplicate, it is checked to see if there is a card with the same ID. If so, it is inserted as a new card with a new ID. If not, it is inserted without a new ID.
This is just so that it passes the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds more like the tests are wrong in this case then? Because that doesn't sound very logical 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but it works fine.
| private long insertCard(Context context, SQLiteDatabase database, LoyaltyCard card, boolean newCard) throws IOException{ | ||
| long newId = 0; | ||
| if (newCard){ | ||
| newId = DBHelper.insertLoyaltyCard(database, 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); | ||
| }else { | ||
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given you only insert cards now if they don't match an existing card, isn't it always with newCard as True? I don't think you need this if/else anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was just so that I wouldn't call the same thing four times. No, newCard is not always true. But I remove this in next commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why newCard will not always be true.
Either it's a duplicate, so you don't insert it.
Or it's not a duplicate, so it gets inserted as a new card.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is inserted, but not always with new ID. This is logic of old implementation.
| Utils.saveCardImage(context, card.getImageThumbnail(context), card.id, ImageLocationType.icon); | ||
| Utils.saveCardImage(context, card.getImageFront(context), card.id, ImageLocationType.front); | ||
| Utils.saveCardImage(context, card.getImageBack(context), card.id, ImageLocationType.back); | ||
| | ||
| return newId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This order seems wrong. You are inserting a card into the database and then get a new ID if it's a new card, but save the images onto the old ID. It seems to me like you'd want to have this order:
- Save card to database and get ID
- Save the images with that new ID
Otherwise, the images may be assigned to the wrong card and still override old images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I wrote above, this code will not be in the next commit.
| I think that this issue can be closed. What do you think @TheLastProject? |
TheLastProject left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still need to actually test this code again (and I barely have time to look really deeply into it), but two issues remain unsolved.
I think this is very close to being merge-worthy though
| public static List<LoyaltyCard> getLoyaltyCardsByCardId(Context context, SQLiteDatabase database, final String cardId) { | ||
| List<LoyaltyCard> cards = new ArrayList<>(); | ||
| | ||
| Cursor data = database.query( | ||
| LoyaltyCardDbIds.TABLE, | ||
| null, | ||
| whereAttrs(LoyaltyCardDbIds.CARD_ID), | ||
| withArgs(cardId), | ||
| null, | ||
| null, | ||
| null | ||
| ); | ||
| | ||
| if (data.moveToFirst()) { | ||
| do { | ||
| LoyaltyCard card = LoyaltyCard.fromCursor(context, data); | ||
| cards.add(card); | ||
| } while (data.moveToNext()); | ||
| } | ||
| | ||
| data.close(); | ||
| return cards; | ||
| } | ||
| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reality is that both will be fairly rare, but the second one doesn't need a new function. And thus is cleaner and easier for long-term maintenance of the codebase.
| if (existing != null && existing.id == card.id) { | ||
| long newId = DBHelper.insertLoyaltyCard(database, 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); | ||
| idMap.put(card.id, (int) newId); | ||
| Utils.saveCardImage(context, card.getImageThumbnail(context), (int) newId, ImageLocationType.icon); | ||
| Utils.saveCardImage(context, card.getImageFront(context), (int) newId, ImageLocationType.front); | ||
| Utils.saveCardImage(context, card.getImageBack(context), (int) newId, ImageLocationType.back); | ||
| }else{ | ||
| 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); | ||
| Utils.saveCardImage(context, card.getImageThumbnail(context), card.id, ImageLocationType.icon); | ||
| Utils.saveCardImage(context, card.getImageFront(context), card.id, ImageLocationType.front); | ||
| Utils.saveCardImage(context, card.getImageBack(context), card.id, ImageLocationType.back); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still insist this code is simply wrong.
Either the card is a duplicate and thus can be skipped.
Or the card is not a duplicate, and thus is inserted as a new card with a new ID.
There is no in-between state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at your implementation before. It's the same code... Same logic as it was before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation before was wrong, otherwise it wouldn't need fixing 😅
| Don't know if this is for Hacktoberfest or not. I don't want you to get "screwed over" for this not being completed in time, so I'll label it hacktoberfest-accepted just in case :) Would be great if the last comments can be looked at, I think this is probably almost correct, but it's clearly already deserved to count from the effort put into it :) |
| @TheLastProject I removed additional function getLoyaltyCardsByCardId and I call instead DB cursor as you wanted. I look at the second bug too. When I removed checking of existing card with the same ID, it created 3 errors in test as last time, but I was not able to fix them. I find out that the problem was in importing cards to group, but I didn't see where. It added card to group, but the card wasn't existing. So number cards in group was higher that in reality. It little bit surprised me, how it's possible that problem is not there when I add this checking. In conclusion, it is good as it is. I have another work too, so in my point of view this issue is done. If you test it, and you will find any duplication of cards or data lost, please let me know and I will fix it. But if this code will work fine, I will be waiting for merge. I'm not involved in Hacktoberfest, but I will be very thankful if you will merge this 😊 |
Sorry, but I really meant the
Okay but a group issue makes sense! See the export documentation. The group part is a list of:
Well, I'm not convinced the code is correct yet, so I don't think it is done and I'm not planning to merge it right now. But if you don't want to keep working on it that's fine, I can take a look at it myself and see if I can figure it out and fix it. Neither of us owes the other free labour :) |




Uh oh!
There was an error while loading. Please reload this page.