From ea456c6d80dfaaeda604801299b88a20f705df2b Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 2 Aug 2024 23:50:40 +0200 Subject: [PATCH 01/11] Add Pkpass parser --- app/build.gradle.kts | 16 +- .../java/protect/card_locker/LoyaltyCard.java | 2 - .../java/protect/card_locker/PkpassParser.kt | 431 ++++++++++++++++++ .../main/java/protect/card_locker/Utils.java | 2 +- .../java/protect/card_locker/ZipUtils.java | 2 +- app/src/main/res/values/strings.xml | 2 + .../java/protect/card_locker/PkpassTest.kt | 238 ++++++++++ .../DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass | Bin 0 -> 20677 bytes .../card_locker/pkpass/DCBLN24/logo.png | Bin 0 -> 13869 bytes .../pkpass/Eurowings/Eurowings.pkpass | Bin 0 -> 26387 bytes .../card_locker/pkpass/Eurowings/logo@2x.png | Bin 0 -> 7689 bytes build.gradle.kts | 1 + 12 files changed, 683 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/PkpassParser.kt create mode 100644 app/src/test/java/protect/card_locker/PkpassTest.kt create mode 100644 app/src/test/res/protect/card_locker/pkpass/DCBLN24/DCBLN24-QLUKT-1-passbook.pkpass create mode 100644 app/src/test/res/protect/card_locker/pkpass/DCBLN24/logo.png create mode 100644 app/src/test/res/protect/card_locker/pkpass/Eurowings/Eurowings.pkpass create mode 100644 app/src/test/res/protect/card_locker/pkpass/Eurowings/logo@2x.png diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 532cb7b44..d3b1bfda8 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.10") 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.10") 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/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index d4e3bf531..2b77ea498 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -11,8 +11,6 @@ import androidx.annotation.Nullable; import java.math.BigDecimal; import java.util.Currency; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; public class LoyaltyCard implements Parcelable { 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 000000000..313196b6c --- /dev/null +++ b/app/src/main/java/protect/card_locker/PkpassParser.kt @@ -0,0 +1,431 @@ +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 + ) + } + + 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 parseColor(color: String): Int? { + // First, try formats supported by Android natively + try { + return Color.parseColor(color) + } catch (ignored: IllegalArgumentException) {} + + // If that didn't work, try parsing it as a rbg(0,0,255) value + val red: Int; + val green: Int; + val blue: Int; + + // Parse rgb(0,0,0) string + val rgbInfo = Regex("""^rgb\(\s*(?\d+)\s*,\s*(?\d+)\s*,\s*(?\d+)\s*\)$""").find(color) + if (rgbInfo == null) { + return null + } + + // Convert to integers + try { + red = rgbInfo.groups[1]!!.value.toInt() + green = rgbInfo.groups[2]!!.value.toInt() + blue = rgbInfo.groups[3]!!.value.toInt() + } catch (e: NumberFormatException) { + return null + } + + // Ensure everything is in a valid range as Color.rgb does not do range checks + if (red < 0 || red > 255) return null + if (green < 0 || green > 255) return null + if (blue < 0 || blue > 255) return null + + return Color.rgb(red, green, blue) + } + + 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 = parseColor(jsonObject.getString("backgroundColor")) + } catch (ignored: JSONException) {} + + 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/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index feee1cc68..0ed45be3f 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -877,7 +877,7 @@ public class Utils { 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 c0c30bd1d..158223dd9 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 class ZipUtils { 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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b7b08e16..881eccf1f 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/PkpassTest.kt b/app/src/test/java/protect/card_locker/PkpassTest.kt new file mode 100644 index 000000000..242e046a1 --- /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 0000000000000000000000000000000000000000..b6d4b08f5cd0a91f6ca903a525612a91dfa29d51 GIT binary patch literal 20677 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..362d1990a52e97c2eb81413994a92ecf06f841f3 GIT binary patch literal 13869 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 Rj)8$xo@pwU$-fEyzX0{a*DwG8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2265f66e1d54a2fdc84f489528eb538c1b29dbc GIT binary patch literal 26387 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ba1a78ff58c5a7854946843c2391cce6ecfdecb3 GIT binary patch literal 7689 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 literal 0 HcmV?d00001 diff --git a/build.gradle.kts b/build.gradle.kts index 3dfc77492..55ba19c93 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.application") version "8.7.3" apply false id("com.github.spotbugs") version "5.1.4" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false } allprojects { From 8eeff0058bc7c788231dd0698a8a32496d2fdc1a Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 1 Oct 2024 17:33:45 +0200 Subject: [PATCH 02/11] Refactor ScanActivity result code (use ParseResult) --- .../protect/card_locker/BarcodeValues.java | 29 ------- ...arcodeValuesListDisambiguatorCallback.java | 6 -- .../java/protect/card_locker/LoyaltyCard.java | 68 +++++++++++------ .../card_locker/LoyaltyCardEditActivity.java | 28 +++---- .../protect/card_locker/MainActivity.java | 40 +++++----- .../java/protect/card_locker/ParseResult.kt | 29 +++++++ .../ParseResultListDisambiguatorCallback.java | 6 ++ .../protect/card_locker/ParseResultType.kt | 6 ++ .../protect/card_locker/ScanActivity.java | 49 +++++------- .../main/java/protect/card_locker/Utils.java | 76 +++++++++++-------- app/src/main/res/values/strings.xml | 1 - .../LoyaltyCardViewActivityTest.java | 24 ++++-- 12 files changed, 197 insertions(+), 165 deletions(-) delete mode 100644 app/src/main/java/protect/card_locker/BarcodeValues.java delete mode 100644 app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java create mode 100644 app/src/main/java/protect/card_locker/ParseResult.kt create mode 100644 app/src/main/java/protect/card_locker/ParseResultListDisambiguatorCallback.java create mode 100644 app/src/main/java/protect/card_locker/ParseResultType.kt diff --git a/app/src/main/java/protect/card_locker/BarcodeValues.java b/app/src/main/java/protect/card_locker/BarcodeValues.java deleted file mode 100644 index 567b6aab3..000000000 --- a/app/src/main/java/protect/card_locker/BarcodeValues.java +++ /dev/null @@ -1,29 +0,0 @@ -package protect.card_locker; - -import androidx.annotation.Nullable; - -public class BarcodeValues { - @Nullable - private final CatimaBarcode mFormat; - private final String mContent; - private String mNote; - - public BarcodeValues(@Nullable CatimaBarcode format, String content) { - mFormat = format; - mContent = content; - } - - public void setNote(String note) { - mNote = note; - } - - public @Nullable CatimaBarcode format() { - return mFormat; - } - - public String content() { - return mContent; - } - - public String note() { return mNote; } -} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java b/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java deleted file mode 100644 index c5c55395d..000000000 --- a/app/src/main/java/protect/card_locker/BarcodeValuesListDisambiguatorCallback.java +++ /dev/null @@ -1,6 +0,0 @@ -package protect.card_locker; - -public interface BarcodeValuesListDisambiguatorCallback { - void onUserChoseBarcode(BarcodeValues barcodeValues); - void onUserDismissedSelector(); -} diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 2b77ea498..03473f9ea 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import java.math.BigDecimal; import java.util.Currency; import java.util.Date; +import java.util.List; import java.util.Objects; public class LoyaltyCard implements Parcelable { @@ -227,6 +228,7 @@ public class LoyaltyCard implements Parcelable { parcel.writeInt(archiveStatus); } + @NonNull public static LoyaltyCard fromBundle(Bundle bundle, boolean requireFull) { // Grab default card LoyaltyCard loyaltyCard = new LoyaltyCard(); @@ -238,7 +240,7 @@ public class LoyaltyCard implements Parcelable { 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,34 +323,56 @@ public class LoyaltyCard implements Parcelable { } } - 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 (expiry != null) { - bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry.getTime()); + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STORE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_STORE, store); } - 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_NOTE)) { + bundle.putString(BUNDLE_LOYALTY_CARD_NOTE, note); } - 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_VALID_FROM)) { + bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom != null ? validFrom.getTime() : -1); } - if (headerColor != null) { - bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor); + 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 (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STAR_STATUS)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_STAR_STATUS, starStatus); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_LAST_USED)) { + bundle.putLong(BUNDLE_LOYALTY_CARD_LAST_USED, lastUsed); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL, zoomLevel); + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS)) { + bundle.putInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS, archiveStatus); } - 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; } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 241e99af7..ecaa208b9 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -693,27 +693,21 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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()); + Bundle resultIntentBundle = resultIntent.getExtras(); + if (resultIntentBundle == null) { + Log.d(TAG, "barcode and card id editor picker returned without a bundle"); + return; + } - Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() { - @Override - public void onUserChoseBarcode(BarcodeValues barcodeValues) { - setLoyaltyCardCardId(barcodeValues.content()); - setLoyaltyCardBarcodeType(barcodeValues.format()); - setLoyaltyCardBarcodeId(""); - } - - @Override - public void onUserDismissedSelector() { - - } - }); + tempLoyaltyCard.updateFromBundle(resultIntentBundle, false); + generateBarcode(); + hasChanged = true; } }); diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index a8e97c298..bc02562f6 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -257,12 +257,9 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard 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 +419,16 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } } - 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); } @@ -459,25 +451,27 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard // Check if an image or file was shared to us if (Intent.ACTION_SEND.equals(receivedAction)) { - List barcodeValuesList; + List parseResultList; if (receivedType.equals("text/plain")) { - barcodeValuesList = Collections.singletonList(new BarcodeValues(null, intent.getStringExtra(Intent.EXTRA_TEXT))); + LoyaltyCard loyaltyCard = new LoyaltyCard(); + loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)); + parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard)); } else if (receivedType.startsWith("image/")) { - barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + parseResultList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); } else if (receivedType.equals("application/pdf")) { - barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + parseResultList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); } else { Log.e(TAG, "Wrong mime-type"); return; } - if (barcodeValuesList.isEmpty()) { + if (parseResultList.isEmpty()) { finish(); return; } - processBarcodeValuesList(barcodeValuesList, null, true); + processParseResultList(parseResultList, null, 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 000000000..190db5ec1 --- /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 + ) + ) + } + } + } +} 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 000000000..50acba584 --- /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 000000000..219644645 --- /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 +} diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index dff50567b..fea6db1cf 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -156,7 +156,7 @@ public class ScanActivity extends CatimaAppCompatActivity { addFromImage(); break; case 3: - addFromPdfFile(); + addFromPdf(); break; default: throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option"); @@ -181,16 +181,11 @@ public class ScanActivity extends CatimaAppCompatActivity { 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 +289,31 @@ public class ScanActivity extends CatimaAppCompatActivity { 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 +360,9 @@ public class ScanActivity extends CatimaAppCompatActivity { // 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,7 +411,7 @@ public class ScanActivity extends CatimaAppCompatActivity { PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE); } - public void addFromPdfFile() { + public void addFromPdf() { PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF); } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 0ed45be3f..349174c0e 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -79,6 +79,7 @@ import java.util.Collections; 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; @@ -144,7 +145,7 @@ public class Utils { 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) { @@ -163,7 +164,7 @@ public class Utils { return new ArrayList<>(); } - List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); + List barcodesFromBitmap = getBarcodesFromBitmap(bitmap); if (barcodesFromBitmap.isEmpty()) { Log.i(TAG, "No barcode found in image file"); @@ -173,7 +174,7 @@ public class Utils { return barcodesFromBitmap; } - static public List retrieveBarcodesFromPdf(Context context, Uri uri) { + 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"); @@ -183,7 +184,7 @@ public class Utils { ParcelFileDescriptor parcelFileDescriptor = null; PdfRenderer renderer = null; - List barcodesFromPdfPages = new ArrayList<>(); + List barcodesFromPdfPages = new ArrayList<>(); try { parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r"); @@ -205,10 +206,10 @@ public class Utils { 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); } } } @@ -237,17 +238,17 @@ public class Utils { } /** - * 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; @@ -276,7 +277,15 @@ public class Utils { 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"); @@ -296,7 +305,7 @@ public class Utils { 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 { @@ -311,7 +320,7 @@ public class Utils { 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()); @@ -320,7 +329,7 @@ public class Utils { 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); @@ -331,37 +340,42 @@ public class Utils { 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); } @@ -371,7 +385,7 @@ public class Utils { builder.setTitle(context.getString(R.string.multipleBarcodesFoundPleaseChooseOne)); builder.setItems( barcodeDescriptions, - (dialogInterface, i) -> callback.onUserChoseBarcode(barcodeValuesList.get(i)) + (dialogInterface, i) -> callback.onUserChoseParseResult(parseResultList.get(i)) ); builder.setOnCancelListener(dialogInterface -> callback.onUserDismissedSelector()); builder.show(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 881eccf1f..fe94854b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,6 +360,5 @@ 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/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index eb61e0316..cada3eba0 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -231,10 +231,14 @@ public class LoyaltyCardViewActivityTest { 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 @@ public class LoyaltyCardViewActivityTest { 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( From 711ca1e7617c24f40db1584ade6d3fde96fdd9bd Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 13 Oct 2024 17:07:58 +0200 Subject: [PATCH 03/11] Add option to load pkpass from ScanActivity --- .../protect/card_locker/ScanActivity.java | 18 +++++++- .../main/java/protect/card_locker/Utils.java | 43 ++++++++++++++++--- .../main/res/drawable/local_activity_24px.xml | 10 +++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/local_activity_24px.xml diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index fea6db1cf..f2b7ef135 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 @@ public class ScanActivity extends CatimaAppCompatActivity { 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 @@ public class ScanActivity extends CatimaAppCompatActivity { 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"}; @@ -158,6 +163,9 @@ public class ScanActivity extends CatimaAppCompatActivity { case 3: addFromPdf(); break; + case 4: + addFromPkPass(); + break; default: throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option"); } @@ -415,6 +423,10 @@ public class ScanActivity extends CatimaAppCompatActivity { 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); @@ -504,12 +516,14 @@ public class ScanActivity extends CatimaAppCompatActivity { } 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/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 349174c0e..4fb9e1441 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -97,12 +97,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)$"; @@ -174,6 +175,32 @@ public class Utils { return barcodesFromBitmap; } + 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) { @@ -264,6 +291,10 @@ public class Utils { 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"); 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 000000000..21c31ee00 --- /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 fe94854b8..881eccf1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,5 +360,6 @@ Export cancelled Use front image Use back image + Select a Passbook file (.pkpass) This file is not supported From 93583487958f1a0eee9807c7e5e6bf35f5aa6c53 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 13 Oct 2024 17:40:40 +0200 Subject: [PATCH 04/11] Add option to share pkpass file to Catima --- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/protect/card_locker/MainActivity.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a07cd9d1..ee705524c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,7 @@ + Date: Wed, 16 Oct 2024 20:15:37 +0200 Subject: [PATCH 05/11] Support opening supported barcode files directly --- app/src/main/AndroidManifest.xml | 13 ++++- .../protect/card_locker/MainActivity.java | 53 ++++++++++++------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee705524c..ccf5521a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,9 +40,20 @@ - + + + + + + + + + + + + diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 3463d2f4a..78f0880af 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.Intent; 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; @@ -449,32 +450,48 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard 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 parseResultList; + if (receivedAction == null || receivedType == null) { + return; + } - if (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 if (receivedType.startsWith("image/")) { - parseResultList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + List parseResultList; + + // 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 action type to parse intent"); + return; + } + + if (receivedType.startsWith("image/")) { + parseResultList = Utils.retrieveBarcodesFromImage(this, data); } else if (receivedType.equals("application/pdf")) { - parseResultList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + parseResultList = Utils.retrieveBarcodesFromPdf(this, data); } else if (receivedType.equals("application/vnd.apple.pkpass")) { - parseResultList = Utils.retrieveBarcodesFromPkPass(this, intent.getParcelableExtra(Intent.EXTRA_STREAM)); + parseResultList = Utils.retrieveBarcodesFromPkPass(this, data); } else { Log.e(TAG, "Wrong mime-type"); return; } - - if (parseResultList.isEmpty()) { - finish(); - return; - } - - processParseResultList(parseResultList, 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) { From 1cb9ddecaca494415b26744e81b1756dc3905a40 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 14 Nov 2024 23:31:30 +0100 Subject: [PATCH 06/11] Support for returning images from PkpassParser --- .../card_locker/CardShortcutConfigure.java | 2 +- .../CardsOnPowerScreenService.java | 4 +- .../java/protect/card_locker/DBHelper.java | 8 +- .../protect/card_locker/ImportURIHelper.java | 21 +- .../java/protect/card_locker/LoyaltyCard.java | 224 ++++++++++++------ .../card_locker/LoyaltyCardCursorAdapter.java | 6 +- .../card_locker/LoyaltyCardEditActivity.java | 190 ++++----------- .../card_locker/LoyaltyCardViewActivity.java | 2 +- .../protect/card_locker/MainActivity.java | 2 +- .../card_locker/ManageGroupCursorAdapter.java | 2 +- .../java/protect/card_locker/ParseResult.kt | 6 +- .../java/protect/card_locker/PkpassParser.kt | 5 +- .../protect/card_locker/ScanActivity.java | 2 +- .../protect/card_locker/ShortcutHelper.java | 2 +- .../main/java/protect/card_locker/Utils.java | 5 +- .../importexport/CatimaExporter.java | 12 +- .../importexport/CatimaImporter.java | 23 +- .../importexport/FidmeImporter.java | 21 +- .../importexport/StocardImporter.java | 21 +- .../importexport/VoucherVaultImporter.java | 21 +- .../protect/card_locker/DatabaseTest.java | 16 +- .../protect/card_locker/ImportExportTest.java | 72 +++--- .../protect/card_locker/ImportURITest.java | 9 +- .../LoyaltyCardCursorAdapterTest.java | 20 +- .../LoyaltyCardViewActivityTest.java | 18 +- 25 files changed, 402 insertions(+), 312 deletions(-) diff --git a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java index 38e0d71d5..eb534c186 100644 --- a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java +++ b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java @@ -66,7 +66,7 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo private void onClickAction(int position) { Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All); selected.moveToPosition(position); - LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(selected); + LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(CardShortcutConfigure.this, selected); Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id); diff --git a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java index 1e21fc6b8..b010b4b79 100644 --- a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java +++ b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java @@ -42,7 +42,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService { 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 class CardsOnPowerScreenService extends ControlsProviderService { 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 7fd52962d..927b2ef32 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 class DBHelper extends SQLiteOpenHelper { 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 class DBHelper extends SQLiteOpenHelper { 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 b733d71d4..61bcff3bc 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 class ImportURIHelper { 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 03473f9ea..d656c6d00 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -1,9 +1,9 @@ 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; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,7 +14,7 @@ import java.util.Date; import java.util.List; import java.util.Objects; -public class LoyaltyCard implements Parcelable { +public class LoyaltyCard { public int id; public String store; public String note; @@ -37,6 +37,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"; @@ -52,6 +59,13 @@ 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"; + + private static final String TEMP_IMAGE_THUMBNAIL_FILE_NAME = "loyaltyCardTempImageThumbnailFileName"; + private static final String TEMP_IMAGE_FRONT_FILE_NAME = "loyaltyCardTempImageFrontFileName"; + private static final String TEMP_IMAGE_BACK_FILE_NAME = "loyaltyCardTempImageBackFileName"; /** * Create a loyalty card object with default values @@ -72,6 +86,9 @@ public class LoyaltyCard implements Parcelable { setLastUsed(Utils.getUnixTime()); setZoomLevel(100); setArchiveStatus(0); + setImageThumbnail(null); + setImageFront(null); + setImageBack(null); } /** @@ -97,7 +114,8 @@ public class LoyaltyCard implements Parcelable { @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); @@ -113,6 +131,9 @@ public class LoyaltyCard implements Parcelable { setLastUsed(lastUsed); setZoomLevel(zoomLevel); setArchiveStatus(archiveStatus); + setImageThumbnail(imageThumbnail); + setImageFront(imageFront); + setImageBack(imageBack); } public void setId(int id) { @@ -187,60 +208,32 @@ public class LoyaltyCard implements Parcelable { this.archiveStatus = archiveStatus; } - protected LoyaltyCard(Parcel in) { - setId(in.readInt()); - setStore(Objects.requireNonNull(in.readString())); - setNote(Objects.requireNonNull(in.readString())); - long tmpValidFrom = in.readLong(); - setValidFrom(tmpValidFrom > 0 ? new Date(tmpValidFrom) : null); - long tmpExpiry = in.readLong(); - setExpiry(tmpExpiry > 0 ? new Date(tmpExpiry) : null); - setBalance((BigDecimal) in.readValue(BigDecimal.class.getClassLoader())); - setBalanceType((Currency) in.readValue(Currency.class.getClassLoader())); - setCardId(Objects.requireNonNull(in.readString())); - setBarcodeId(in.readString()); - String tmpBarcodeType = in.readString(); - setBarcodeType((tmpBarcodeType != null && !tmpBarcodeType.isEmpty()) ? CatimaBarcode.fromName(tmpBarcodeType) : null); - int tmpHeaderColor = in.readInt(); - setHeaderColor(tmpHeaderColor != -1 ? tmpHeaderColor : null); - setStarStatus(in.readInt()); - setLastUsed(in.readLong()); - setZoomLevel(in.readInt()); - setArchiveStatus(in.readInt()); + public void setImageThumbnail(@Nullable Bitmap imageThumbnail) { + this.imageThumbnail = imageThumbnail; } - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeInt(id); - parcel.writeString(store); - parcel.writeString(note); - parcel.writeLong(validFrom != null ? validFrom.getTime() : -1); - parcel.writeLong(expiry != null ? expiry.getTime() : -1); - parcel.writeValue(balance); - parcel.writeValue(balanceType); - parcel.writeString(cardId); - parcel.writeString(barcodeId); - parcel.writeString(barcodeType != null ? barcodeType.name() : ""); - parcel.writeInt(headerColor != null ? headerColor : -1); - parcel.writeInt(starStatus); - parcel.writeLong(lastUsed); - parcel.writeInt(zoomLevel); - parcel.writeInt(archiveStatus); + public void setImageFront(@Nullable Bitmap imageFront) { + this.imageFront = imageFront; } - @NonNull - public static LoyaltyCard fromBundle(Bundle bundle, boolean requireFull) { - // Grab default card - LoyaltyCard loyaltyCard = new LoyaltyCard(); - - // Update from bundle - loyaltyCard.updateFromBundle(bundle, requireFull); - - // Return updated version - return loyaltyCard; + public void setImageBack(@Nullable Bitmap imageBack) { + this.imageBack = imageBack; } - public void updateFromBundle(@NonNull Bundle bundle, boolean requireFull) { + @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"); + } + + public void updateFromBundle(@NonNull Context context, @NonNull Bundle bundle, boolean requireFull) { if (bundle.containsKey(BUNDLE_LOYALTY_CARD_ID)) { setId(bundle.getInt(BUNDLE_LOYALTY_CARD_ID)); } else if (requireFull) { @@ -321,9 +314,39 @@ public class LoyaltyCard implements Parcelable { } else if (requireFull) { throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS); } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { + String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL); + if (tempImageName != null) { + setImageThumbnail(Utils.loadTempImage(context, tempImageName)); + } else { + setImageThumbnail(null); + } + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL); + } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) { + String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT); + if (tempImageName != null) { + setImageFront(Utils.loadTempImage(context, tempImageName)); + } else { + setImageFront(null); + } + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_FRONT); + } + if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) { + String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_BACK); + if (tempImageName != null) { + setImageBack(Utils.loadTempImage(context, tempImageName)); + } else { + setImageBack(null); + } + } else if (requireFull) { + throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_BACK); + } } - public Bundle toBundle(List exportLimit) { + public Bundle toBundle(Context context, List exportLimit) { boolean exportIsLimited = !exportLimit.isEmpty(); Bundle bundle = new Bundle(); @@ -373,11 +396,37 @@ public class LoyaltyCard implements Parcelable { if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS)) { bundle.putInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS, archiveStatus); } + // There is an (undocumented) size limit to bundles of around 2MB(?), when going over it you will experience a random crash + // So, instead of storing the bitmaps directly, we write the bitmap to a temp file and store the path + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { + if (imageThumbnail != null) { + Utils.saveTempImage(context, imageThumbnail, TEMP_IMAGE_THUMBNAIL_FILE_NAME, Bitmap.CompressFormat.PNG); + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, TEMP_IMAGE_THUMBNAIL_FILE_NAME); + } else { + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, null); + } + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) { + if (imageFront != null) { + Utils.saveTempImage(context, imageFront, TEMP_IMAGE_FRONT_FILE_NAME, Bitmap.CompressFormat.PNG); + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, TEMP_IMAGE_FRONT_FILE_NAME); + } else { + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, null); + } + } + if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) { + if (imageBack != null) { + Utils.saveTempImage(context, imageBack, TEMP_IMAGE_BACK_FILE_NAME, Bitmap.CompressFormat.PNG); + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, TEMP_IMAGE_BACK_FILE_NAME); + } else { + bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, null); + } + } 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 @@ -414,11 +463,37 @@ public class LoyaltyCard implements Parcelable { int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL)); // archiveStatus int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS)); + // 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); + 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 @@ -433,12 +508,23 @@ public class LoyaltyCard implements Parcelable { 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 } - @Override - public int describeContents() { - return 0; + 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; } @NonNull @@ -447,7 +533,8 @@ public class LoyaltyCard implements Parcelable { 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, @@ -462,19 +549,10 @@ public class LoyaltyCard implements Parcelable { this.starStatus, this.lastUsed, this.zoomLevel, - this.archiveStatus + this.archiveStatus, + this.imageThumbnail, + this.imageFront, + this.imageBack ); } - - public static final Creator CREATOR = new Creator() { - @Override - public LoyaltyCard createFromParcel(Parcel in) { - return new LoyaltyCard(in); - } - - @Override - public LoyaltyCard[] newArray(int size) { - return new LoyaltyCard[size]; - } - }; } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index f43aece3d..446df08bf 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 class LoyaltyCardCursorAdapter extends BaseCursorAdapter 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 +267,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // 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(); @@ -307,7 +286,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // If the intent contains any loyalty card fields, override those fields in our current temp card if (b != null) { - tempLoyaltyCard.updateFromBundle(b, false); + tempLoyaltyCard.updateFromBundle(this, b, false); } Log.d(TAG, "Edit activity: id=" + loyaltyCardId @@ -321,56 +300,28 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements super.onSaveInstanceState(savedInstanceState); tabs = binding.tabs; savedInstanceState.putInt(STATE_TAB_INDEX, tabs.getSelectedTabPosition()); - savedInstanceState.putParcelable(STATE_TEMP_CARD, tempLoyaltyCard); + savedInstanceState.putBundle(STATE_TEMP_CARD, tempLoyaltyCard.toBundle(this, new ArrayList<>())); 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); } @Override public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { onRestoring = true; - tempLoyaltyCard = savedInstanceState.getParcelable(STATE_TEMP_CARD); + tempLoyaltyCard = new LoyaltyCard(); + Bundle tempCardBundle = savedInstanceState.getBundle(STATE_TEMP_CARD); + if (tempCardBundle != null) { + tempLoyaltyCard.updateFromBundle(this, tempCardBundle, true); + } tempLoyaltyCardField = (LoyaltyCardField) savedInstanceState.getSerializable(STATE_TEMP_CARD_FIELD); 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 +376,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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); @@ -705,7 +656,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements return; } - tempLoyaltyCard.updateFromBundle(resultIntentBundle, false); + tempLoyaltyCard.updateFromBundle(this, resultIntentBundle, false); generateBarcode(); hasChanged = true; } @@ -727,20 +678,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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(); @@ -813,26 +757,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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() { @@ -842,40 +774,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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); @@ -961,6 +865,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } } + setThumbnailImage(tempLoyaltyCard.imageThumbnail); + setCardImage(ImageLocationType.front, cardImageFront, tempLoyaltyCard.imageFront, true); + setCardImage(ImageLocationType.back, cardImageBack, tempLoyaltyCard.imageBack, true); + // Initialization has finished if (!initDone) { initDone = true; @@ -1000,7 +908,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } protected void setThumbnailImage(@Nullable Bitmap bitmap) { - setCardImage(thumbnail, bitmap, false); + setCardImage(ImageLocationType.icon, thumbnail, bitmap, false); if (bitmap != null) { int headerColor = Utils.getHeaderColorFromImage(bitmap, Utils.getHeaderColor(this, tempLoyaltyCard)); @@ -1021,8 +929,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } } - 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); @@ -1279,30 +1195,30 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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; }); } @@ -1358,21 +1274,17 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements }); 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; }); @@ -1423,8 +1335,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setLoyaltyCardHeaderColor(color); // Unset image if set - mIconRemoved = true; - mIconUnsaved = false; setThumbnailImage(null); } @@ -1602,16 +1512,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } 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); @@ -1745,7 +1655,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements 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 71d898d2e..fdd0c6594 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 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements 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 78f0880af..c5c7fa8be 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -429,7 +429,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard @Override public void onUserChoseParseResult(ParseResult parseResult) { Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); - Bundle bundle = parseResult.toLoyaltyCardBundle(); + Bundle bundle = parseResult.toLoyaltyCardBundle(MainActivity.this); if (group != null) { bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group); } diff --git a/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java index 258d5ddec..e0f41e0cb 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 class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter { @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 index 190db5ec1..d1bb634a0 100644 --- a/app/src/main/java/protect/card_locker/ParseResult.kt +++ b/app/src/main/java/protect/card_locker/ParseResult.kt @@ -1,5 +1,6 @@ package protect.card_locker +import android.content.Context import android.os.Bundle class ParseResult( @@ -7,9 +8,9 @@ class ParseResult( val loyaltyCard: LoyaltyCard) { var note: String? = null - fun toLoyaltyCardBundle(): Bundle { + fun toLoyaltyCardBundle(context: Context): Bundle { when (parseResultType) { - ParseResultType.FULL -> return loyaltyCard.toBundle(listOf()) + ParseResultType.FULL -> return loyaltyCard.toBundle(context, listOf()) ParseResultType.BARCODE_ONLY -> { val defaultLoyaltyCard = LoyaltyCard() defaultLoyaltyCard.setBarcodeId(null) @@ -17,6 +18,7 @@ class ParseResult( defaultLoyaltyCard.setCardId(loyaltyCard.cardId) return defaultLoyaltyCard.toBundle( + context, listOf( LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_ID, LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_TYPE, diff --git a/app/src/main/java/protect/card_locker/PkpassParser.kt b/app/src/main/java/protect/card_locker/PkpassParser.kt index 313196b6c..d3c325826 100644 --- a/app/src/main/java/protect/card_locker/PkpassParser.kt +++ b/app/src/main/java/protect/card_locker/PkpassParser.kt @@ -125,7 +125,10 @@ class PkpassParser(context: Context, uri: Uri?) { starStatus, lastUsed, zoomLevel, - archiveStatus + archiveStatus, + image, + null, + null ) } diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index f2b7ef135..5aa33a5fa 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -299,7 +299,7 @@ public class ScanActivity extends CatimaAppCompatActivity { private void returnResult(ParseResult parseResult) { Intent result = new Intent(); - Bundle bundle = parseResult.toLoyaltyCardBundle(); + Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this); if (addGroup != null) { bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup); } diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index bd9d7f115..9b93e9598 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -86,7 +86,7 @@ class ShortcutHelper { 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 4fb9e1441..6817605d2 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -176,7 +176,6 @@ public class Utils { } 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"); @@ -836,7 +835,7 @@ public class Utils { } } - public static Bitmap loadImage(String path) { + public static @Nullable Bitmap loadImage(String path) { try { return BitmapFactory.decodeStream(new FileInputStream(path)); } catch (IOException e) { @@ -845,7 +844,7 @@ public class Utils { } } - public static Bitmap loadTempImage(Context context, String name) { + public static @Nullable Bitmap loadTempImage(Context context, String name) { return loadImage(context.getCacheDir() + "/" + name); } 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 530f9c194..c56e75666 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 class CatimaExporter implements Exporter { // 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 class CatimaExporter implements Exporter { 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 @@ public class CatimaExporter implements Exporter { 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 @@ public class CatimaExporter implements Exporter { 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 @@ public class CatimaExporter implements Exporter { 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 cca491f2d..08e3f97a7 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 class CatimaImporter implements Importer { 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 @@ public class CatimaImporter implements Importer { // 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 9275ff203..e25a41d16 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 @@ public class FidmeImporter implements Importer { // 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 94aa98aa2..c022696bb 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 class StocardImporter implements Importer { 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 b45d81efe..94c7ba5e8 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 class VoucherVaultImporter implements Importer { } // 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/test/java/protect/card_locker/DatabaseTest.java b/app/src/test/java/protect/card_locker/DatabaseTest.java index a57b02b04..a0e965c8f 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 class DatabaseTest { 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 c90bff3db..13bf3eb4a 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 @@ public class ImportExportTest { 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 @@ public class ImportExportTest { 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 @@ public class ImportExportTest { 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 @@ public class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { // 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 class ImportExportTest { // 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 class ImportExportTest { 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 492c6ae3e..f0b867827 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 @@ import java.util.Date; @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 class ImportURITest { 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 class ImportURITest { 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 class ImportURITest { 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 1ada6674d..7f647c161 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 @@ public class LoyaltyCardCursorAdapterTest { @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 class LoyaltyCardCursorAdapterTest { @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 class LoyaltyCardCursorAdapterTest { 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 class LoyaltyCardCursorAdapterTest { 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 class LoyaltyCardCursorAdapterTest { 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 class LoyaltyCardCursorAdapterTest { 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 class LoyaltyCardCursorAdapterTest { @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 class LoyaltyCardCursorAdapterTest { @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 class LoyaltyCardCursorAdapterTest { @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 class LoyaltyCardCursorAdapterTest { @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 cada3eba0..ad8a5bc73 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 @@ public class LoyaltyCardViewActivityTest { 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); @@ -216,7 +216,7 @@ public class LoyaltyCardViewActivityTest { * Initiate and complete a barcode capture, either in success * or in failure */ - private void captureBarcodeWithResult(final Activity activity, final boolean success) throws IOException { + private void captureBarcodeWithResult(final Activity activity, final boolean success) { // Start image capture final Button startButton = activity.findViewById(R.id.enterButton); startButton.performClick(); @@ -238,7 +238,7 @@ public class LoyaltyCardViewActivityTest { loyaltyCard.setCardId(BARCODE_DATA); ParseResult parseResult = new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard); - resultIntent.putExtras(parseResult.toLoyaltyCardBundle()); + resultIntent.putExtras(parseResult.toLoyaltyCardBundle(activity)); // Respond to image capture, success shadowOf(activity).receiveResult( @@ -251,7 +251,7 @@ public class LoyaltyCardViewActivityTest { * Initiate and complete a barcode selection, either in success * or in failure */ - private void selectBarcodeWithResult(final Activity activity, final String barcodeData, final String barcodeType, final boolean success) throws IOException { + private void selectBarcodeWithResult(final Activity activity, final String barcodeData, final String barcodeType, final boolean success) { // Start barcode selector final Button startButton = activity.findViewById(R.id.enterButton); startButton.performClick(); @@ -278,7 +278,7 @@ public class LoyaltyCardViewActivityTest { loyaltyCard.setCardId(barcodeData); ParseResult parseResult = new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard); - resultIntent.putExtras(parseResult.toLoyaltyCardBundle()); + resultIntent.putExtras(parseResult.toLoyaltyCardBundle(activity)); // Respond to barcode selection, success shadowOf(activity).receiveResult( @@ -302,8 +302,10 @@ public class LoyaltyCardViewActivityTest { } else if (fieldType == FieldTypeView.ImageView) { ImageView imageView = (ImageView) view; Bitmap image = null; - if (imageView.getTag() != null) { + try { image = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); + } catch (ClassCastException e) { + // This is probably a VectorDrawable, the placeholder image. Aka: No image. } assertEquals(contents, image); } else { @@ -419,8 +421,8 @@ public class LoyaltyCardViewActivityTest { cardIdField.setText("12345678"); barcodeField.setText("87654321"); barcodeTypeField.setText(CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE).prettyName()); - activity.setCardImage(frontImageView, frontBitmap, true); - activity.setCardImage(backImageView, backBitmap, true); + activity.setCardImage(ImageLocationType.front, frontImageView, frontBitmap, true); + activity.setCardImage(ImageLocationType.back, backImageView, backBitmap, true); shadowOf(getMainLooper()).idle(); From 96a9850d9c076caee351d89275c9b708f9576d88 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 20 Nov 2024 23:05:02 +0100 Subject: [PATCH 07/11] Delete old cache files on startup --- .../protect/card_locker/MainActivity.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index c5c7fa8be..b2ae6d809 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -35,6 +35,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; +import java.io.File; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -201,6 +202,28 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard protected void onCreate(Bundle inputSavedInstanceState) { SplashScreen.installSplashScreen(this); super.onCreate(inputSavedInstanceState); + + // Delete old cache files + // These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc. + new Thread(() -> { + long twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24); + + File[] tempFiles = getCacheDir().listFiles(); + + if (tempFiles == null) { + Log.e(TAG, "getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup..."); + return; + } + + for (File file : tempFiles) { + if (file.lastModified() < twentyFourHoursAgo) { + if (!file.delete()) { + Log.w(TAG, "Failed to delete cache file " + file.getPath()); + } + }; + } + }).start(); + // We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash extractIntentFields(getIntent()); From 83fca93649e56d3055fb688441130e6659b52238 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 26 Nov 2024 22:32:15 +0100 Subject: [PATCH 08/11] Use ViewModel to prevent hammering storage When you turn a LoyaltyCard into a bundle, it writes the files to storage as it can't otherwise fit in the limited storage size. This means that, on rotation, you write all images to storage and load them again. Using a ViewModel prevents that storage hit due to holding it in memory (as a ViewModel has a longer lifecycle). --- .../card_locker/LoyaltyCardEditActivity.java | 359 ++++++++---------- .../LoyaltyCardEditActivityViewModel.kt | 24 ++ app/src/main/res/values/strings.xml | 1 + .../LoyaltyCardViewActivityTest.java | 12 +- 4 files changed, 199 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 4ed0b2524..1320b9da6 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -45,6 +45,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; +import androidx.lifecycle.ViewModelProvider; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; @@ -85,17 +86,12 @@ import java.util.concurrent.Callable; import protect.card_locker.async.TaskHandler; import protect.card_locker.databinding.LayoutChipChoiceBinding; import protect.card_locker.databinding.LoyaltyCardEditActivityBinding; +import protect.card_locker.viewmodels.LoyaltyCardEditActivityViewModel; public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback, ColorPickerDialogListener { - private LoyaltyCardEditActivityBinding binding; private static final String TAG = "Catima"; - - private final String STATE_TAB_INDEX = "savedTab"; - private final String STATE_TEMP_CARD = "tempLoyaltyCard"; - private final String STATE_TEMP_CARD_FIELD = "tempLoyaltyCardField"; - private final String STATE_UPDATE_LOYALTY_CARD = "updateLoyaltyCard"; - private final String STATE_HAS_CHANGED = "hasChange"; - private final String STATE_OPEN_SET_ICON_MENU = "openSetIconMenu"; + protected LoyaltyCardEditActivityViewModel viewModel; + private LoyaltyCardEditActivityBinding binding; private static final String PICK_DATE_REQUEST_KEY = "pick_date_request"; private static final String NEWLY_PICKED_DATE_ARGUMENT_KEY = "newly_picked_date"; @@ -117,8 +113,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements public static final String BUNDLE_OPEN_SET_ICON_MENU = "openSetIconMenu"; public static final String BUNDLE_ADDGROUP = "addGroup"; - TabLayout tabs; - ImageView thumbnail; ImageView thumbnailEditIcon; EditText storeFieldEdit; @@ -143,17 +137,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements Toolbar toolbar; - int loyaltyCardId; - boolean updateLoyaltyCard; - boolean duplicateFromLoyaltyCardId; - boolean openSetIconMenu; - String addGroup; - - Uri importLoyaltyCardUri = null; - SQLiteDatabase mDatabase; - boolean hasChanged = false; String tempStoredOldBarcodeValue = null; boolean initDone = false; boolean onResuming = false; @@ -164,15 +149,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements HashMap currencies = new HashMap<>(); HashMap currencySymbols = new HashMap<>(); - LoyaltyCard tempLoyaltyCard = new LoyaltyCard(); - LoyaltyCardField tempLoyaltyCardField; - ActivityResultLauncher mPhotoTakerLauncher; ActivityResultLauncher mPhotoPickerLauncher; ActivityResultLauncher mCardIdAndBarCodeEditorLauncher; ActivityResultLauncher mCropperLauncher; - int mRequestedImage = 0; UCrop.Options mCropperOptions; final private TaskHandler mTasks = new TaskHandler(); @@ -188,95 +169,100 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } protected void setLoyaltyCardStore(@NonNull String store) { - tempLoyaltyCard.setStore(store); + viewModel.getLoyaltyCard().setStore(store); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardNote(@NonNull String note) { - tempLoyaltyCard.setNote(note); + viewModel.getLoyaltyCard().setNote(note); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardValidFrom(@Nullable Date validFrom) { - tempLoyaltyCard.setValidFrom(validFrom); + viewModel.getLoyaltyCard().setValidFrom(validFrom); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardExpiry(@Nullable Date expiry) { - tempLoyaltyCard.setExpiry(expiry); + viewModel.getLoyaltyCard().setExpiry(expiry); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) { - tempLoyaltyCard.setBalance(balance); + viewModel.getLoyaltyCard().setBalance(balance); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) { - tempLoyaltyCard.setBalanceType(balanceType); + viewModel.getLoyaltyCard().setBalanceType(balanceType); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardCardId(@NonNull String cardId) { - tempLoyaltyCard.setCardId(cardId); + viewModel.getLoyaltyCard().setCardId(cardId); generateBarcode(); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardBarcodeId(@Nullable String barcodeId) { - tempLoyaltyCard.setBarcodeId(barcodeId); + viewModel.getLoyaltyCard().setBarcodeId(barcodeId); generateBarcode(); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardBarcodeType(@Nullable CatimaBarcode barcodeType) { - tempLoyaltyCard.setBarcodeType(barcodeType); + viewModel.getLoyaltyCard().setBarcodeType(barcodeType); generateBarcode(); - hasChanged = true; + viewModel.setHasChanged(true); } protected void setLoyaltyCardHeaderColor(@Nullable Integer headerColor) { - tempLoyaltyCard.setHeaderColor(headerColor); + viewModel.getLoyaltyCard().setHeaderColor(headerColor); - hasChanged = true; + viewModel.setHasChanged(true); } /* Extract intent fields and return if code should keep running */ private boolean extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); - addGroup = b != null ? b.getString(BUNDLE_ADDGROUP) : null; - openSetIconMenu = b != null && b.getBoolean(BUNDLE_OPEN_SET_ICON_MENU, false); + viewModel.setAddGroup(b != null ? b.getString(BUNDLE_ADDGROUP) : null); + viewModel.setOpenSetIconMenu(b != null && b.getBoolean(BUNDLE_OPEN_SET_ICON_MENU, false)); - loyaltyCardId = b != null ? b.getInt(BUNDLE_ID) : 0; - updateLoyaltyCard = b != null && b.getBoolean(BUNDLE_UPDATE, false); - duplicateFromLoyaltyCardId = b != null && b.getBoolean(BUNDLE_DUPLICATE_ID, false); - importLoyaltyCardUri = intent.getData(); + viewModel.setLoyaltyCardId(b != null ? b.getInt(BUNDLE_ID) : 0); + viewModel.setUpdateLoyaltyCard(b != null && b.getBoolean(BUNDLE_UPDATE, false)); + viewModel.setDuplicateFromLoyaltyCardId(b != null && b.getBoolean(BUNDLE_DUPLICATE_ID, false)); + viewModel.setImportLoyaltyCardUri(intent.getData()); + + Uri importLoyaltyCardUri = viewModel.getImportLoyaltyCardUri(); // If we have to import a loyalty card, do so - if (updateLoyaltyCard || duplicateFromLoyaltyCardId) { - tempLoyaltyCard = DBHelper.getLoyaltyCard(this, mDatabase, loyaltyCardId); - if (tempLoyaltyCard == null) { - Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId); + if (viewModel.getUpdateLoyaltyCard() || viewModel.getDuplicateFromLoyaltyCardId()) { + // Retrieve from database + LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(this, mDatabase, viewModel.getLoyaltyCardId()); + if (loyaltyCard == null) { + Log.w(TAG, "Could not lookup loyalty card " + viewModel.getLoyaltyCardId()); Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show(); finish(); return false; } + viewModel.setLoyaltyCard(loyaltyCard); } else if (importLoyaltyCardUri != null) { + // Load from URI try { - tempLoyaltyCard = new ImportURIHelper(this).parse(importLoyaltyCardUri); + viewModel.setLoyaltyCard(new ImportURIHelper(this).parse(importLoyaltyCardUri)); } catch (InvalidObjectException ex) { Toast.makeText(this, R.string.failedParsingImportUriError, Toast.LENGTH_LONG).show(); finish(); @@ -286,11 +272,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // If the intent contains any loyalty card fields, override those fields in our current temp card if (b != null) { - tempLoyaltyCard.updateFromBundle(this, b, false); + LoyaltyCard loyaltyCard = viewModel.getLoyaltyCard(); + loyaltyCard.updateFromBundle(this, b, false); + viewModel.setLoyaltyCard(loyaltyCard); } - Log.d(TAG, "Edit activity: id=" + loyaltyCardId - + ", updateLoyaltyCard=" + updateLoyaltyCard); + Log.d(TAG, "Edit activity: id=" + viewModel.getLoyaltyCardId() + + ", updateLoyaltyCard=" + viewModel.getUpdateLoyaltyCard()); return true; } @@ -298,31 +286,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements @Override public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); - tabs = binding.tabs; - savedInstanceState.putInt(STATE_TAB_INDEX, tabs.getSelectedTabPosition()); - savedInstanceState.putBundle(STATE_TEMP_CARD, tempLoyaltyCard.toBundle(this, new ArrayList<>())); - savedInstanceState.putSerializable(STATE_TEMP_CARD_FIELD, tempLoyaltyCardField); - - savedInstanceState.putInt(STATE_UPDATE_LOYALTY_CARD, updateLoyaltyCard ? 1 : 0); - savedInstanceState.putInt(STATE_HAS_CHANGED, hasChanged ? 1 : 0); - savedInstanceState.putInt(STATE_OPEN_SET_ICON_MENU, openSetIconMenu ? 1 : 0); } @Override public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { onRestoring = true; - tempLoyaltyCard = new LoyaltyCard(); - Bundle tempCardBundle = savedInstanceState.getBundle(STATE_TEMP_CARD); - if (tempCardBundle != null) { - tempLoyaltyCard.updateFromBundle(this, tempCardBundle, true); - } - tempLoyaltyCardField = (LoyaltyCardField) savedInstanceState.getSerializable(STATE_TEMP_CARD_FIELD); super.onRestoreInstanceState(savedInstanceState); - tabs = binding.tabs; - tabs.selectTab(tabs.getTabAt(savedInstanceState.getInt(STATE_TAB_INDEX))); - updateLoyaltyCard = savedInstanceState.getInt(STATE_UPDATE_LOYALTY_CARD) == 1; - hasChanged = savedInstanceState.getInt(STATE_HAS_CHANGED) == 1; - openSetIconMenu = savedInstanceState.getInt(STATE_OPEN_SET_ICON_MENU) == 1; } @Override @@ -330,14 +299,20 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements super.onCreate(savedInstanceState); binding = LoyaltyCardEditActivityBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + + viewModel = new ViewModelProvider(this).get(LoyaltyCardEditActivityViewModel.class); + toolbar = binding.toolbar; setSupportActionBar(toolbar); enableToolbarBackButton(); mDatabase = new DBHelper(this).getWritableDatabase(); - if (!extractIntentFields(getIntent())) { - return; + if (!viewModel.getInitialized()) { + if (!extractIntentFields(getIntent())) { + return; + } + viewModel.setInitialized(true); } for (Currency currency : Currency.getAvailableCurrencies()) { @@ -345,7 +320,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements currencySymbols.put(currency.getCurrencyCode(), currency.getSymbol()); } - tabs = binding.tabs; thumbnail = binding.thumbnail; thumbnailEditIcon = binding.thumbnailEditIcon; storeFieldEdit = binding.storeNameEdit; @@ -403,7 +377,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setLoyaltyCardBalance(BigDecimal.valueOf(0)); } - balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, tempLoyaltyCard.balanceType)); + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType)); } }); @@ -412,7 +386,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements public void onTextChanged(CharSequence s, int start, int before, int count) { if (onResuming || onRestoring) return; try { - BigDecimal balance = Utils.parseBalance(s.toString(), tempLoyaltyCard.balanceType); + BigDecimal balance = Utils.parseBalance(s.toString(), viewModel.getLoyaltyCard().balanceType); setLoyaltyCardBalance(balance); balanceField.setError(null); validBalance = true; @@ -437,8 +411,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setLoyaltyCardBalanceType(currency); - if (tempLoyaltyCard.balance != null && !onResuming && !onRestoring) { - balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, currency)); + if (viewModel.getLoyaltyCard().balance != null && !onResuming && !onRestoring) { + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, currency)); } } @@ -484,7 +458,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // We changed the card ID, save the current barcode ID in a temp // variable and make sure to ask the user later if they also want to // update the barcode ID - if (tempLoyaltyCard.barcodeId != null) { + if (viewModel.getLoyaltyCard().barcodeId != null) { // If it is not set to "same as Card ID", save as tempStoredOldBarcodeValue tempStoredOldBarcodeValue = barcodeIdField.getText().toString(); } @@ -539,8 +513,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements input.setLayoutParams(params); container.addView(input); - if (tempLoyaltyCard.barcodeId != null) { - input.setText(tempLoyaltyCard.barcodeId); + if (viewModel.getLoyaltyCard().barcodeId != null) { + input.setText(viewModel.getLoyaltyCard().barcodeId); } builder.setView(container); @@ -601,10 +575,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } }); - tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + binding.tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public void onTabSelected(TabLayout.Tab tab) { + viewModel.setTabIndex(tab.getPosition()); showPart(tab.getText().toString()); } @@ -616,12 +591,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements @Override @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public void onTabReselected(TabLayout.Tab tab) { + viewModel.setTabIndex(tab.getPosition()); showPart(tab.getText().toString()); } }); - tabs.selectTab(tabs.getTabAt(0)); - + selectTab(viewModel.getTabIndex()); mPhotoTakerLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), result -> { if (result) { @@ -656,9 +631,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements return; } - tempLoyaltyCard.updateFromBundle(this, resultIntentBundle, false); + LoyaltyCard loyaltyCard = viewModel.getLoyaltyCard(); + loyaltyCard.updateFromBundle(this, resultIntentBundle, false); + viewModel.setLoyaltyCard(loyaltyCard); generateBarcode(); - hasChanged = true; + viewModel.setHasChanged(true); } }); @@ -681,11 +658,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setCardImage(ImageLocationType.front, cardImageFront, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); } else if (requestedBackImage()) { setCardImage(ImageLocationType.back, cardImageBack, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); - } else { + } else if (requestedIcon()) { setThumbnailImage(Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_SMALL)); + } else { + Toast.makeText(this, R.string.generic_error_please_retry, Toast.LENGTH_LONG).show(); + return; } - Log.d("cropper", "mRequestedImage: " + mRequestedImage); - hasChanged = true; + Log.d("cropper", "requestedImageType: " + viewModel.getRequestedImageType()); + viewModel.setHasChanged(true); } else { Toast.makeText(LoyaltyCardEditActivity.this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); } @@ -708,6 +688,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements }); } + private void selectTab(int index) { + binding.tabs.selectTab(binding.tabs.getTabAt(index)); + viewModel.setTabIndex(index); + } + // ucrop 2.2.6 initial aspect ratio is glitched when 0x0 is used as the initial ratio option // https://github.com/Yalantis/uCrop/blob/281c8e6438d81f464d836fc6b500517144af264a/ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java#L264 // so source width height has to be provided for now, depending on whether future versions of ucrop will support 0x0 as the default option @@ -745,24 +730,22 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } } - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - Log.i(TAG, "Received new intent"); - extractIntentFields(intent); - } - private boolean requestedFrontImage() { - return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_FRONT; + int requestedImageType = viewModel.getRequestedImageType(); + + return requestedImageType == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || requestedImageType == Utils.CARD_IMAGE_FROM_FILE_FRONT; } private boolean requestedBackImage() { - return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_BACK || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_BACK; + int requestedImageType = viewModel.getRequestedImageType(); + + return requestedImageType == Utils.CARD_IMAGE_FROM_CAMERA_BACK || requestedImageType == Utils.CARD_IMAGE_FROM_FILE_BACK; } private boolean requestedIcon() { - return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_ICON || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_ICON; + int requestedImageType = viewModel.getRequestedImageType(); + + return requestedImageType == Utils.CARD_IMAGE_FROM_CAMERA_ICON || requestedImageType == Utils.CARD_IMAGE_FROM_FILE_ICON; } @SuppressLint("DefaultLocale") @@ -770,39 +753,41 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements protected void onResume() { super.onResume(); - Log.i(TAG, "To view card: " + loyaltyCardId); + Log.i(TAG, "To view card: " + viewModel.getLoyaltyCardId()); onResuming = true; - if (updateLoyaltyCard) { + if (viewModel.getUpdateLoyaltyCard()) { setTitle(R.string.editCardTitle); } else { setTitle(R.string.addCardTitle); } - boolean hadChanges = hasChanged; + boolean hadChanges = viewModel.getHasChanged(); - storeFieldEdit.setText(tempLoyaltyCard.store); - noteFieldEdit.setText(tempLoyaltyCard.note); - formatDateField(this, validFromField, tempLoyaltyCard.validFrom); - formatDateField(this, expiryField, tempLoyaltyCard.expiry); - cardIdFieldView.setText(tempLoyaltyCard.cardId); - barcodeIdField.setText(tempLoyaltyCard.barcodeId != null ? tempLoyaltyCard.barcodeId : getString(R.string.sameAsCardId)); - barcodeTypeField.setText(tempLoyaltyCard.barcodeType != null ? tempLoyaltyCard.barcodeType.prettyName() : getString(R.string.noBarcode)); + storeFieldEdit.setText(viewModel.getLoyaltyCard().store); + noteFieldEdit.setText(viewModel.getLoyaltyCard().note); + formatDateField(this, validFromField, viewModel.getLoyaltyCard().validFrom); + formatDateField(this, expiryField, viewModel.getLoyaltyCard().expiry); + cardIdFieldView.setText(viewModel.getLoyaltyCard().cardId); + String barcodeId = viewModel.getLoyaltyCard().barcodeId; + barcodeIdField.setText(barcodeId != null && !barcodeId.isEmpty() ? barcodeId : getString(R.string.sameAsCardId)); + CatimaBarcode barcodeType = viewModel.getLoyaltyCard().barcodeType; + barcodeTypeField.setText(barcodeType != null ? barcodeType.prettyName() : getString(R.string.noBarcode)); // We set the balance here (with onResuming/onRestoring == true) to prevent formatBalanceCurrencyField() from setting it (via onTextChanged), // which can cause issues when switching locale because it parses the balance and e.g. the decimal separator may have changed. - formatBalanceCurrencyField(tempLoyaltyCard.balanceType); - BigDecimal balance = tempLoyaltyCard.balance == null ? new BigDecimal("0") : tempLoyaltyCard.balance; + formatBalanceCurrencyField(viewModel.getLoyaltyCard().balanceType); + BigDecimal balance = viewModel.getLoyaltyCard().balance == null ? new BigDecimal("0") : viewModel.getLoyaltyCard().balance; setLoyaltyCardBalance(balance); - balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(tempLoyaltyCard.balance, tempLoyaltyCard.balanceType)); + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType)); validBalance = true; Log.d(TAG, "Setting balance to " + balance); if (groupsChips.getChildCount() == 0) { List existingGroups = DBHelper.getGroups(mDatabase); - List loyaltyCardGroups = DBHelper.getLoyaltyCardGroups(mDatabase, loyaltyCardId); + List loyaltyCardGroups = DBHelper.getLoyaltyCardGroups(mDatabase, viewModel.getLoyaltyCardId()); if (existingGroups.isEmpty()) { groupsChips.setVisibility(View.GONE); @@ -817,7 +802,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements chip.setText(group._id); chip.setTag(group); - if (group._id.equals(addGroup)) { + if (group._id.equals(viewModel.getAddGroup())) { chip.setChecked(true); } else { chip.setChecked(false); @@ -830,7 +815,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } chip.setOnTouchListener((v, event) -> { - hasChanged = true; + viewModel.setHasChanged(true); return false; }); @@ -839,40 +824,19 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } } - if (tempLoyaltyCard.headerColor == null) { + if (viewModel.getLoyaltyCard().headerColor == null) { // If name is set, pick colour relevant for name. Otherwise pick randomly - setLoyaltyCardHeaderColor(tempLoyaltyCard.store.isEmpty() ? Utils.getRandomHeaderColor(this) : Utils.getHeaderColor(this, tempLoyaltyCard)); + setLoyaltyCardHeaderColor(viewModel.getLoyaltyCard().store.isEmpty() ? Utils.getRandomHeaderColor(this) : Utils.getHeaderColor(this, viewModel.getLoyaltyCard())); } - // Fix up some fields - if (tempLoyaltyCard.barcodeType != null) { - try { - barcodeTypeField.setText(tempLoyaltyCard.barcodeType.prettyName()); - } catch (IllegalArgumentException e) { - barcodeTypeField.setText(getString(R.string.noBarcode)); - } - } - - if (tempLoyaltyCard.cardId != null) { - cardIdFieldView.setText(tempLoyaltyCard.cardId); - } - - if (tempLoyaltyCard.barcodeId != null) { - if (!tempLoyaltyCard.barcodeId.isEmpty()) { - barcodeIdField.setText(tempLoyaltyCard.barcodeId); - } else { - barcodeIdField.setText(getString(R.string.sameAsCardId)); - } - } - - setThumbnailImage(tempLoyaltyCard.imageThumbnail); - setCardImage(ImageLocationType.front, cardImageFront, tempLoyaltyCard.imageFront, true); - setCardImage(ImageLocationType.back, cardImageBack, tempLoyaltyCard.imageBack, true); + setThumbnailImage(viewModel.getLoyaltyCard().imageThumbnail); + setCardImage(ImageLocationType.front, cardImageFront, viewModel.getLoyaltyCard().imageFront, true); + setCardImage(ImageLocationType.back, cardImageBack, viewModel.getLoyaltyCard().imageBack, true); // Initialization has finished if (!initDone) { initDone = true; - hasChanged = hadChanges; + viewModel.setHasChanged(hadChanges); } generateBarcode(); @@ -889,7 +853,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements generateIcon(storeFieldEdit.getText().toString().trim()); - Integer headerColor = tempLoyaltyCard.headerColor; + Integer headerColor = viewModel.getLoyaltyCard().headerColor; if (headerColor != null) { thumbnail.setOnClickListener(new ChooseCardImage()); thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE); @@ -901,8 +865,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // Fake click on the edit icon to cause the set icon option to pop up if the icon was // long-pressed in the view activity - if (openSetIconMenu) { - openSetIconMenu = false; + if (viewModel.getOpenSetIconMenu()) { + viewModel.setOpenSetIconMenu(false); thumbnail.callOnClick(); } } @@ -911,7 +875,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setCardImage(ImageLocationType.icon, thumbnail, bitmap, false); if (bitmap != null) { - int headerColor = Utils.getHeaderColorFromImage(bitmap, Utils.getHeaderColor(this, tempLoyaltyCard)); + int headerColor = Utils.getHeaderColorFromImage(bitmap, Utils.getHeaderColor(this, viewModel.getLoyaltyCard())); setLoyaltyCardHeaderColor(headerColor); @@ -922,20 +886,22 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } else { generateIcon(storeFieldEdit.getText().toString().trim()); - if (tempLoyaltyCard.headerColor != null) { - thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(tempLoyaltyCard.headerColor) ? Color.BLACK : Color.WHITE); - thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(tempLoyaltyCard.headerColor) ? Color.WHITE : Color.BLACK); + Integer headerColor = viewModel.getLoyaltyCard().headerColor; + + if (headerColor != null) { + thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE); + thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK); } } } protected void setCardImage(ImageLocationType imageLocationType, ImageView imageView, Bitmap bitmap, boolean applyFallback) { if (imageLocationType == ImageLocationType.icon) { - tempLoyaltyCard.setImageThumbnail(bitmap); + viewModel.getLoyaltyCard().setImageThumbnail(bitmap); } else if (imageLocationType == ImageLocationType.front) { - tempLoyaltyCard.setImageFront(bitmap); + viewModel.getLoyaltyCard().setImageFront(bitmap); } else if (imageLocationType == ImageLocationType.back) { - tempLoyaltyCard.setImageBack(bitmap); + viewModel.getLoyaltyCard().setImageBack(bitmap); } else { throw new IllegalArgumentException("Unknown image type"); } @@ -1118,7 +1084,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } private void askBeforeQuitIfChanged() { - if (!hasChanged) { + if (!viewModel.getHasChanged()) { if (tempStoredOldBarcodeValue != null) { askBarcodeChange(this::askBeforeQuitIfChanged); return; @@ -1145,7 +1111,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private void takePhotoForCard(int type) { Uri photoURI = FileProvider.getUriForFile(LoyaltyCardEditActivity.this, BuildConfig.APPLICATION_ID, Utils.createTempFile(this, TEMP_CAMERA_IMAGE_NAME)); - mRequestedImage = type; + viewModel.setRequestedImageType(type); try { mPhotoTakerLauncher.launch(photoURI); @@ -1156,7 +1122,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } private void selectImageFromGallery(int type) { - mRequestedImage = type; + viewModel.setRequestedImageType(type); Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); @@ -1200,15 +1166,15 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements ImageView targetView; if (v.getId() == R.id.frontImageHolder) { - currentImage = tempLoyaltyCard.imageFront; + currentImage = viewModel.getLoyaltyCard().imageFront; imageLocationType = ImageLocationType.front; targetView = cardImageFront; } else if (v.getId() == R.id.backImageHolder) { - currentImage = tempLoyaltyCard.imageBack; + currentImage = viewModel.getLoyaltyCard().imageBack; imageLocationType = ImageLocationType.back; targetView = cardImageBack; } else if (v.getId() == R.id.thumbnail) { - currentImage = tempLoyaltyCard.imageThumbnail; + currentImage = viewModel.getLoyaltyCard().imageThumbnail; imageLocationType = ImageLocationType.icon; targetView = thumbnail; } else { @@ -1227,8 +1193,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements cardOptions.put(getString(R.string.selectColor), () -> { ColorPickerDialog.Builder dialogBuilder = ColorPickerDialog.newBuilder(); - if (tempLoyaltyCard.headerColor != null) { - dialogBuilder.setColor(tempLoyaltyCard.headerColor); + if (viewModel.getLoyaltyCard().headerColor != null) { + dialogBuilder.setColor(viewModel.getLoyaltyCard().headerColor); } ColorPickerDialog dialog = dialogBuilder.create(); @@ -1274,17 +1240,17 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements }); if (v.getId() == R.id.thumbnail) { - if (tempLoyaltyCard.imageFront != null) { + if (viewModel.getLoyaltyCard().imageFront != null) { cardOptions.put(getString(R.string.useFrontImage), () -> { - setThumbnailImage(Utils.resizeBitmap(tempLoyaltyCard.imageFront, Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(viewModel.getLoyaltyCard().imageFront, Utils.BITMAP_SIZE_SMALL)); return null; }); } - if (tempLoyaltyCard.imageBack != null) { + if (viewModel.getLoyaltyCard().imageBack != null) { cardOptions.put(getString(R.string.useBackImage), () -> { - setThumbnailImage(Utils.resizeBitmap(tempLoyaltyCard.imageBack, Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(viewModel.getLoyaltyCard().imageBack, Utils.BITMAP_SIZE_SMALL)); return null; }); @@ -1385,7 +1351,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // Required to handle configuration changes // See https://github.com/material-components/material-components-android/issues/1688 - tempLoyaltyCardField = loyaltyCardField; + viewModel.setTempLoyaltyCardField(loyaltyCardField); getSupportFragmentManager().addFragmentOnAttachListener((fragmentManager, fragment) -> { if (fragment instanceof MaterialDatePicker && Objects.equals(fragment.getTag(), PICK_DATE_REQUEST_KEY)) { ((MaterialDatePicker) fragment).addOnPositiveButtonClickListener(selection -> { @@ -1418,6 +1384,12 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements long selection = result.getLong(NEWLY_PICKED_DATE_ARGUMENT_KEY); Date newDate = new Date(selection); + + LoyaltyCardField tempLoyaltyCardField = viewModel.getTempLoyaltyCardField(); + if (tempLoyaltyCardField == null) { + throw new AssertionError("tempLoyaltyCardField is null unexpectedly!"); + } + switch (tempLoyaltyCardField) { case validFrom: formatDateField(LoyaltyCardEditActivity.this, validFromField, newDate); @@ -1459,22 +1431,22 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements boolean hasError = false; - if (tempLoyaltyCard.store.isEmpty()) { + if (viewModel.getLoyaltyCard().store.isEmpty()) { storeFieldEdit.setError(getString(R.string.field_must_not_be_empty)); // Focus element - tabs.selectTab(tabs.getTabAt(0)); + selectTab(0); storeFieldEdit.requestFocus(); hasError = true; } - if (tempLoyaltyCard.cardId.isEmpty()) { + if (viewModel.getLoyaltyCard().cardId.isEmpty()) { cardIdFieldView.setError(getString(R.string.field_must_not_be_empty)); // Focus element if first error element if (!hasError) { - tabs.selectTab(tabs.getTabAt(0)); + selectTab(0); cardIdFieldView.requestFocus(); hasError = true; } @@ -1485,7 +1457,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // Focus element if first error element if (!hasError) { - tabs.selectTab(tabs.getTabAt(1)); + selectTab(1); balanceField.requestFocus(); hasError = true; } @@ -1505,25 +1477,25 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // Both update and new card save with lastUsed set to null // This makes the DBHelper set it to the current date // So that new and edited card are always on top when sorting by recently used - if (updateLoyaltyCard) { - DBHelper.updateLoyaltyCard(mDatabase, loyaltyCardId, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.validFrom, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, tempLoyaltyCard.starStatus, null, tempLoyaltyCard.archiveStatus); + if (viewModel.getUpdateLoyaltyCard()) { + DBHelper.updateLoyaltyCard(mDatabase, viewModel.getLoyaltyCardId(), viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, viewModel.getLoyaltyCard().starStatus, null, viewModel.getLoyaltyCard().archiveStatus); } else { - loyaltyCardId = (int) DBHelper.insertLoyaltyCard(mDatabase, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.validFrom, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, 0, null, 0); + viewModel.setLoyaltyCardId((int) DBHelper.insertLoyaltyCard(mDatabase, viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, 0, null, 0)); } try { - Utils.saveCardImage(this, tempLoyaltyCard.imageFront, loyaltyCardId, ImageLocationType.front); - Utils.saveCardImage(this, tempLoyaltyCard.imageBack, loyaltyCardId, ImageLocationType.back); - Utils.saveCardImage(this, tempLoyaltyCard.imageThumbnail, loyaltyCardId, ImageLocationType.icon); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageFront, viewModel.getLoyaltyCardId(), ImageLocationType.front); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageBack, viewModel.getLoyaltyCardId(), ImageLocationType.back); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageThumbnail, viewModel.getLoyaltyCardId(), ImageLocationType.icon); } catch (FileNotFoundException e) { e.printStackTrace(); } - DBHelper.setLoyaltyCardGroups(mDatabase, loyaltyCardId, selectedGroups); + DBHelper.setLoyaltyCardGroups(mDatabase, viewModel.getLoyaltyCardId(), selectedGroups); - ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, loyaltyCardId)); + ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, viewModel.getLoyaltyCardId())); - if (duplicateFromLoyaltyCardId) { + if (viewModel.getDuplicateFromLoyaltyCardId()) { Intent intent = new Intent(getApplicationContext(), MainActivity.class); startActivity(intent); } @@ -1566,6 +1538,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements mCropperOptions.setToolbarTitle(getResources().getString(R.string.setBackImage)); } else if (requestedIcon()) { mCropperOptions.setToolbarTitle(getResources().getString(R.string.setIcon)); + } else { + Toast.makeText(this, R.string.generic_error_please_retry, Toast.LENGTH_LONG).show(); + return; } if (requestedIcon()) { @@ -1618,8 +1593,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private void generateBarcode() { mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); - String cardIdString = tempLoyaltyCard.barcodeId != null ? tempLoyaltyCard.barcodeId : tempLoyaltyCard.cardId; - CatimaBarcode barcodeFormat = tempLoyaltyCard.barcodeType; + String cardIdString = viewModel.getLoyaltyCard().barcodeId != null ? viewModel.getLoyaltyCard().barcodeId : viewModel.getLoyaltyCard().cardId; + CatimaBarcode barcodeFormat = viewModel.getLoyaltyCard().barcodeType; if (cardIdString == null || cardIdString.isEmpty() || barcodeFormat == null) { barcodeImageLayout.setVisibility(View.GONE); @@ -1651,14 +1626,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } private void generateIcon(String store) { - if (tempLoyaltyCard.headerColor == null) { + Integer headerColor = viewModel.getLoyaltyCard().headerColor; + + if (headerColor == null) { return; } - if (tempLoyaltyCard.imageThumbnail == null) { - thumbnail.setBackgroundColor(tempLoyaltyCard.headerColor); + if (viewModel.getLoyaltyCard().imageThumbnail == null) { + thumbnail.setBackgroundColor(headerColor); - LetterBitmap letterBitmap = Utils.generateIcon(this, store, tempLoyaltyCard.headerColor); + LetterBitmap letterBitmap = Utils.generateIcon(this, store, headerColor); if (letterBitmap != null) { thumbnail.setImageBitmap(letterBitmap.getLetterTile()); diff --git a/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt b/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt new file mode 100644 index 000000000..1faeb0f53 --- /dev/null +++ b/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt @@ -0,0 +1,24 @@ +package protect.card_locker.viewmodels + +import android.net.Uri +import androidx.lifecycle.ViewModel +import protect.card_locker.LoyaltyCard +import protect.card_locker.LoyaltyCardField + +class LoyaltyCardEditActivityViewModel : ViewModel() { + var initialized: Boolean = false + var hasChanged: Boolean = false + + var addGroup: String? = null + var openSetIconMenu: Boolean = false + var loyaltyCardId: Int = 0 + var updateLoyaltyCard: Boolean = false + var duplicateFromLoyaltyCardId: Boolean = false + var importLoyaltyCardUri: Uri? = null + + var tabIndex: Int = 0 + var requestedImageType: Int = 0 + var tempLoyaltyCardField: LoyaltyCardField? = null + + var loyaltyCard: LoyaltyCard = LoyaltyCard() +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 881eccf1f..4f09ac5aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -362,4 +362,5 @@ Use back image Select a Passbook file (.pkpass) This file is not supported + Sorry, something went wrong, please try again... diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index ad8a5bc73..07f9af1bd 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -585,13 +585,13 @@ public class LoyaltyCardViewActivityTest { // A change was made shadowOf(activity).clickMenuItem(android.R.id.home); assertEquals(true, activity.confirmExitDialog.isShowing()); - assertEquals(true, activity.hasChanged); + assertEquals(true, activity.viewModel.getHasChanged()); assertEquals(false, activity.isFinishing()); // Exit after setting hasChanged to false - activity.hasChanged = false; + activity.viewModel.setHasChanged(false); shadowOf(activity).clickMenuItem(android.R.id.home); - assertEquals(false, activity.hasChanged); + assertEquals(false, activity.viewModel.getHasChanged()); assertEquals(true, activity.isFinishing()); } @@ -708,13 +708,13 @@ public class LoyaltyCardViewActivityTest { // A change was made shadowOf(activity).clickMenuItem(android.R.id.home); assertEquals(true, activity.confirmExitDialog.isShowing()); - assertEquals(true, activity.hasChanged); + assertEquals(true, activity.viewModel.getHasChanged()); assertEquals(false, activity.isFinishing()); // Exit after setting hasChanged to false - activity.hasChanged = false; + activity.viewModel.setHasChanged(false); shadowOf(activity).clickMenuItem(android.R.id.home); - assertEquals(false, activity.hasChanged); + assertEquals(false, activity.viewModel.getHasChanged()); assertEquals(true, activity.isFinishing()); database.close(); From 7fe67960bf3ec15ced5bf8fd595a9a9dc7cb3fe8 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 26 Nov 2024 22:40:46 +0100 Subject: [PATCH 09/11] Move TaskHandler to ViewModel This should make it possible to properly cancel the running barcode generation threads on rotation and prevent CPU rising on many rotations. --- .../java/protect/card_locker/LoyaltyCardEditActivity.java | 8 +++----- .../viewmodels/LoyaltyCardEditActivityViewModel.kt | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 1320b9da6..1c25d2a52 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -156,8 +156,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements ActivityResultLauncher mCropperLauncher; UCrop.Options mCropperOptions; - final private TaskHandler mTasks = new TaskHandler(); - // store system locale for Build.VERSION.SDK_INT < Build.VERSION_CODES.N private Locale mSystemLocale; @@ -1591,7 +1589,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } private void generateBarcode() { - mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); + viewModel.getTaskHandler().flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); String cardIdString = viewModel.getLoyaltyCard().barcodeId != null ? viewModel.getLoyaltyCard().barcodeId : viewModel.getLoyaltyCard().cardId; CatimaBarcode barcodeFormat = viewModel.getLoyaltyCard().barcodeType; @@ -1615,13 +1613,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements Log.d(TAG, "ImageView size now known"); BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, LoyaltyCardEditActivity.this, true); - mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + viewModel.getTaskHandler().executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } }); } else { Log.d(TAG, "ImageView size known known, creating barcode"); BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, this, true); - mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + viewModel.getTaskHandler().executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); } } diff --git a/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt b/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt index 1faeb0f53..32f689ef4 100644 --- a/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt +++ b/app/src/main/java/protect/card_locker/viewmodels/LoyaltyCardEditActivityViewModel.kt @@ -4,11 +4,14 @@ import android.net.Uri import androidx.lifecycle.ViewModel import protect.card_locker.LoyaltyCard import protect.card_locker.LoyaltyCardField +import protect.card_locker.async.TaskHandler class LoyaltyCardEditActivityViewModel : ViewModel() { var initialized: Boolean = false var hasChanged: Boolean = false + var taskHandler: TaskHandler = TaskHandler(); + var addGroup: String? = null var openSetIconMenu: Boolean = false var loyaltyCardId: Int = 0 From e0786594bc8d9b14c20d911bfd66018f11fe199e Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 1 Dec 2024 13:12:44 +0100 Subject: [PATCH 10/11] Load images on request This prevents loading the front and back images when scrolling through the loyalty card list and should allow scaling to more images/files more easily --- .../java/protect/card_locker/DBHelper.java | 2 +- .../protect/card_locker/ImportURIHelper.java | 3 + .../java/protect/card_locker/LoyaltyCard.java | 185 ++++++++++++------ .../card_locker/LoyaltyCardEditActivity.java | 40 ++-- .../java/protect/card_locker/PkpassParser.kt | 3 + .../importexport/CatimaExporter.java | 2 +- .../importexport/CatimaImporter.java | 5 +- .../importexport/FidmeImporter.java | 3 + .../importexport/StocardImporter.java | 3 + .../importexport/VoucherVaultImporter.java | 3 + .../LoyaltyCardViewActivityTest.java | 7 +- 11 files changed, 173 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 927b2ef32..418e517c9 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -335,7 +335,7 @@ public class DBHelper extends SQLiteOpenHelper { LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor); for (ImageLocationType imageLocationType : ImageLocationType.values()) { String name = Utils.getCardImageFileName(card.id, imageLocationType); - if (card.getImageForImageLocationType(imageLocationType) != null) { + if (card.getImageForImageLocationType(context, imageLocationType) != null) { files.add(name); } } diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index 61bcff3bc..73de97403 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -143,6 +143,9 @@ public class ImportURIHelper { 0, null, null, + null, + null, + null, null ); } catch (NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) { diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index d656c6d00..5216cd8da 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -38,11 +38,17 @@ public class LoyaltyCard { public int archiveStatus; @Nullable - public Bitmap imageThumbnail; + private Bitmap imageThumbnail; @Nullable - public Bitmap imageFront; + private String imageThumbnailPath; @Nullable - public Bitmap imageBack; + private Bitmap imageFront; + @Nullable + private String imageFrontPath; + @Nullable + private Bitmap imageBack; + @Nullable + private String imageBackPath; public static final String BUNDLE_LOYALTY_CARD_ID = "loyaltyCardId"; public static final String BUNDLE_LOYALTY_CARD_STORE = "loyaltyCardStore"; @@ -86,9 +92,9 @@ public class LoyaltyCard { setLastUsed(Utils.getUnixTime()); setZoomLevel(100); setArchiveStatus(0); - setImageThumbnail(null); - setImageFront(null); - setImageBack(null); + setImageThumbnail(null, null); + setImageFront(null, null); + setImageBack(null, null); } /** @@ -115,7 +121,9 @@ public class LoyaltyCard { 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, - @Nullable Bitmap imageThumbnail, @Nullable Bitmap imageFront, @Nullable Bitmap imageBack) { + @Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath, + @Nullable Bitmap imageFront, @Nullable String imageFrontPath, + @Nullable Bitmap imageBack, @Nullable String imageBackPath) { setId(id); setStore(store); setNote(note); @@ -131,9 +139,63 @@ public class LoyaltyCard { setLastUsed(lastUsed); setZoomLevel(zoomLevel); setArchiveStatus(archiveStatus); - setImageThumbnail(imageThumbnail); - setImageFront(imageFront); - setImageBack(imageBack); + setImageThumbnail(imageThumbnail, imageThumbnailPath); + setImageFront(imageFront, imageFrontPath); + setImageBack(imageBack, imageBackPath); + } + + @Nullable + public Bitmap getImageThumbnail(Context context) { + if (imageThumbnailPath != null) { + if (imageThumbnailPath.equals(TEMP_IMAGE_THUMBNAIL_FILE_NAME)) { + imageThumbnail = Utils.loadTempImage(context, imageThumbnailPath); + } else { + imageThumbnail = Utils.retrieveCardImage(context, imageThumbnailPath); + } + imageThumbnailPath = null; + } + + if (imageThumbnail == null) { + return null; + } + + return imageThumbnail.copy(imageThumbnail.getConfig(), imageThumbnail.isMutable()); + } + + @Nullable + public Bitmap getImageFront(Context context) { + if (imageFrontPath != null) { + if (imageFrontPath.equals(TEMP_IMAGE_FRONT_FILE_NAME)) { + imageFront = Utils.loadTempImage(context, imageFrontPath); + } else { + imageFront = Utils.retrieveCardImage(context, imageFrontPath); + } + imageFrontPath = null; + } + + if (imageFront == null) { + return null; + } + + return imageFront.copy(imageFront.getConfig(), imageFront.isMutable()); + } + + @Nullable + public Bitmap getImageBack(Context context) { + if (imageBackPath != null) { + if (imageBackPath.equals(TEMP_IMAGE_BACK_FILE_NAME)) { + imageBack = Utils.loadTempImage(context, imageBackPath); + } else { + imageBack = Utils.retrieveCardImage(context, imageBackPath); + } + imageBackPath = null; + } + + if (imageBack == null) { + return null; + } + + return imageBack.copy(imageBack.getConfig(), imageBack.isMutable()); } public void setId(int id) { @@ -208,32 +270,47 @@ public class LoyaltyCard { this.archiveStatus = archiveStatus; } - public void setImageThumbnail(@Nullable Bitmap imageThumbnail) { - this.imageThumbnail = imageThumbnail; + public void setImageThumbnail(@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath) { + if (imageThumbnail != null && imageThumbnailPath != null) { + throw new IllegalArgumentException("Cannot set both thumbnail and path"); + } + + this.imageThumbnailPath = imageThumbnailPath; + this.imageThumbnail = imageThumbnail != null ? imageThumbnail.copy(imageThumbnail.getConfig(), imageThumbnail.isMutable()) : null; } - public void setImageFront(@Nullable Bitmap imageFront) { - this.imageFront = imageFront; + public void setImageFront(@Nullable Bitmap imageFront, @Nullable String imageFrontPath) { + if (imageFront != null && imageFrontPath != null) { + throw new IllegalArgumentException("Cannot set both thumbnail and path"); + } + + this.imageFrontPath = imageFrontPath; + this.imageFront = imageFront != null ? imageFront.copy(imageFront.getConfig(), imageFront.isMutable()) : null; } - public void setImageBack(@Nullable Bitmap imageBack) { - this.imageBack = imageBack; + public void setImageBack(@Nullable Bitmap imageBack, @Nullable String imageBackPath) { + if (imageBack != null && imageBackPath != null) { + throw new IllegalArgumentException("Cannot set both thumbnail and path"); + } + + this.imageBackPath = imageBackPath; + this.imageBack = imageBack != null ? imageBack.copy(imageBack.getConfig(), imageBack.isMutable()) : null; } @Nullable - public Bitmap getImageForImageLocationType(ImageLocationType imageLocationType) { + public Bitmap getImageForImageLocationType(Context context, ImageLocationType imageLocationType) { if (imageLocationType == ImageLocationType.icon) { - return imageThumbnail; + return getImageThumbnail(context); } else if (imageLocationType == ImageLocationType.front) { - return imageFront; + return getImageFront(context); } else if (imageLocationType == ImageLocationType.back) { - return imageBack; + return getImageBack(context); } throw new IllegalArgumentException("Unknown image location type"); } - public void updateFromBundle(@NonNull Context context, @NonNull 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) { @@ -315,32 +392,17 @@ public class LoyaltyCard { throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS); } if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { - String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL); - if (tempImageName != null) { - setImageThumbnail(Utils.loadTempImage(context, tempImageName)); - } else { - setImageThumbnail(null); - } + setImageThumbnail(null, bundle.getString(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)) { - String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT); - if (tempImageName != null) { - setImageFront(Utils.loadTempImage(context, tempImageName)); - } else { - setImageFront(null); - } + setImageFront(null, bundle.getString(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)) { - String tempImageName = bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_BACK); - if (tempImageName != null) { - setImageBack(Utils.loadTempImage(context, tempImageName)); - } else { - setImageBack(null); - } + setImageBack(null, bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_BACK)); } else if (requireFull) { throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_BACK); } @@ -399,24 +461,27 @@ public class LoyaltyCard { // There is an (undocumented) size limit to bundles of around 2MB(?), when going over it you will experience a random crash // So, instead of storing the bitmaps directly, we write the bitmap to a temp file and store the path if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) { - if (imageThumbnail != null) { - Utils.saveTempImage(context, imageThumbnail, TEMP_IMAGE_THUMBNAIL_FILE_NAME, Bitmap.CompressFormat.PNG); + Bitmap thumbnail = getImageThumbnail(context); + if (thumbnail != null) { + Utils.saveTempImage(context, thumbnail, TEMP_IMAGE_THUMBNAIL_FILE_NAME, Bitmap.CompressFormat.PNG); bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, TEMP_IMAGE_THUMBNAIL_FILE_NAME); } else { bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, null); } } if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) { - if (imageFront != null) { - Utils.saveTempImage(context, imageFront, TEMP_IMAGE_FRONT_FILE_NAME, Bitmap.CompressFormat.PNG); + Bitmap front = getImageFront(context); + if (front != null) { + Utils.saveTempImage(context, front, TEMP_IMAGE_FRONT_FILE_NAME, Bitmap.CompressFormat.PNG); bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, TEMP_IMAGE_FRONT_FILE_NAME); } else { bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, null); } } if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) { - if (imageBack != null) { - Utils.saveTempImage(context, imageBack, TEMP_IMAGE_BACK_FILE_NAME, Bitmap.CompressFormat.PNG); + Bitmap back = getImageBack(context); + if (back != null) { + Utils.saveTempImage(context, back, TEMP_IMAGE_BACK_FILE_NAME, Bitmap.CompressFormat.PNG); bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, TEMP_IMAGE_BACK_FILE_NAME); } else { bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, null); @@ -463,12 +528,6 @@ public class LoyaltyCard { int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL)); // archiveStatus int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS)); - // 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, @@ -486,13 +545,16 @@ public class LoyaltyCard { lastUsed, zoomLevel, archiveStatus, - imageThumbnail, - imageFront, - imageBack + null, + Utils.getCardImageFileName(id, ImageLocationType.icon), + null, + Utils.getCardImageFileName(id, ImageLocationType.front), + null, + Utils.getCardImageFileName(id, ImageLocationType.back) ); } - public static boolean isDuplicate(final LoyaltyCard a, final LoyaltyCard b) { + public static boolean isDuplicate(Context context, 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 @@ -509,9 +571,9 @@ public class LoyaltyCard { Utils.equals(a.headerColor, b.headerColor) && // nullable Integer a.starStatus == b.starStatus && // 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 + nullableBitmapsEqual(a.getImageThumbnail(context), b.getImageThumbnail(context)) && // nullable Bitmap + nullableBitmapsEqual(a.getImageFront(context), b.getImageFront(context)) && // nullable Bitmap + nullableBitmapsEqual(a.getImageBack(context), b.getImageBack(context)); // nullable Bitmap } public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap b) { @@ -534,7 +596,7 @@ public class LoyaltyCard { "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" - + " imageThumbnail=%s,%n imageFront=%s,%n imageBack=%s,%n}", + + " imageThumbnail=%s,%n imageThumbnailPath=%s,%n imageFront=%s,%n imageFrontPath=%s,%n imageBack=%s,%n imageBackPath=%s,%n}", this.id, this.store, this.note, @@ -551,8 +613,11 @@ public class LoyaltyCard { this.zoomLevel, this.archiveStatus, this.imageThumbnail, + this.imageThumbnailPath, this.imageFront, - this.imageBack + this.imageFrontPath, + this.imageBack, + this.imageBackPath ); } } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 1c25d2a52..4028f6f08 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -271,7 +271,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // If the intent contains any loyalty card fields, override those fields in our current temp card if (b != null) { LoyaltyCard loyaltyCard = viewModel.getLoyaltyCard(); - loyaltyCard.updateFromBundle(this, b, false); + loyaltyCard.updateFromBundle(b, false); viewModel.setLoyaltyCard(loyaltyCard); } @@ -630,7 +630,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } LoyaltyCard loyaltyCard = viewModel.getLoyaltyCard(); - loyaltyCard.updateFromBundle(this, resultIntentBundle, false); + loyaltyCard.updateFromBundle(resultIntentBundle, false); viewModel.setLoyaltyCard(loyaltyCard); generateBarcode(); viewModel.setHasChanged(true); @@ -827,9 +827,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements setLoyaltyCardHeaderColor(viewModel.getLoyaltyCard().store.isEmpty() ? Utils.getRandomHeaderColor(this) : Utils.getHeaderColor(this, viewModel.getLoyaltyCard())); } - setThumbnailImage(viewModel.getLoyaltyCard().imageThumbnail); - setCardImage(ImageLocationType.front, cardImageFront, viewModel.getLoyaltyCard().imageFront, true); - setCardImage(ImageLocationType.back, cardImageBack, viewModel.getLoyaltyCard().imageBack, true); + setThumbnailImage(viewModel.getLoyaltyCard().getImageThumbnail(this)); + setCardImage(ImageLocationType.front, cardImageFront, viewModel.getLoyaltyCard().getImageFront(this), true); + setCardImage(ImageLocationType.back, cardImageBack, viewModel.getLoyaltyCard().getImageBack(this), true); // Initialization has finished if (!initDone) { @@ -895,11 +895,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements protected void setCardImage(ImageLocationType imageLocationType, ImageView imageView, Bitmap bitmap, boolean applyFallback) { if (imageLocationType == ImageLocationType.icon) { - viewModel.getLoyaltyCard().setImageThumbnail(bitmap); + viewModel.getLoyaltyCard().setImageThumbnail(bitmap, null); } else if (imageLocationType == ImageLocationType.front) { - viewModel.getLoyaltyCard().setImageFront(bitmap); + viewModel.getLoyaltyCard().setImageFront(bitmap, null); } else if (imageLocationType == ImageLocationType.back) { - viewModel.getLoyaltyCard().setImageBack(bitmap); + viewModel.getLoyaltyCard().setImageBack(bitmap, null); } else { throw new IllegalArgumentException("Unknown image type"); } @@ -1164,15 +1164,15 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements ImageView targetView; if (v.getId() == R.id.frontImageHolder) { - currentImage = viewModel.getLoyaltyCard().imageFront; + currentImage = viewModel.getLoyaltyCard().getImageFront(LoyaltyCardEditActivity.this); imageLocationType = ImageLocationType.front; targetView = cardImageFront; } else if (v.getId() == R.id.backImageHolder) { - currentImage = viewModel.getLoyaltyCard().imageBack; + currentImage = viewModel.getLoyaltyCard().getImageBack(LoyaltyCardEditActivity.this); imageLocationType = ImageLocationType.back; targetView = cardImageBack; } else if (v.getId() == R.id.thumbnail) { - currentImage = viewModel.getLoyaltyCard().imageThumbnail; + currentImage = viewModel.getLoyaltyCard().getImageThumbnail(LoyaltyCardEditActivity.this); imageLocationType = ImageLocationType.icon; targetView = thumbnail; } else { @@ -1238,17 +1238,19 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements }); if (v.getId() == R.id.thumbnail) { - if (viewModel.getLoyaltyCard().imageFront != null) { + Bitmap imageFront = viewModel.getLoyaltyCard().getImageFront(LoyaltyCardEditActivity.this); + if (imageFront != null) { cardOptions.put(getString(R.string.useFrontImage), () -> { - setThumbnailImage(Utils.resizeBitmap(viewModel.getLoyaltyCard().imageFront, Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(imageFront, Utils.BITMAP_SIZE_SMALL)); return null; }); } - if (viewModel.getLoyaltyCard().imageBack != null) { + Bitmap imageBack = viewModel.getLoyaltyCard().getImageBack(LoyaltyCardEditActivity.this); + if (imageBack != null) { cardOptions.put(getString(R.string.useBackImage), () -> { - setThumbnailImage(Utils.resizeBitmap(viewModel.getLoyaltyCard().imageBack, Utils.BITMAP_SIZE_SMALL)); + setThumbnailImage(Utils.resizeBitmap(imageBack, Utils.BITMAP_SIZE_SMALL)); return null; }); @@ -1482,9 +1484,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements } try { - Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageFront, viewModel.getLoyaltyCardId(), ImageLocationType.front); - Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageBack, viewModel.getLoyaltyCardId(), ImageLocationType.back); - Utils.saveCardImage(this, viewModel.getLoyaltyCard().imageThumbnail, viewModel.getLoyaltyCardId(), ImageLocationType.icon); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().getImageFront(this), viewModel.getLoyaltyCardId(), ImageLocationType.front); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().getImageBack(this), viewModel.getLoyaltyCardId(), ImageLocationType.back); + Utils.saveCardImage(this, viewModel.getLoyaltyCard().getImageThumbnail(this), viewModel.getLoyaltyCardId(), ImageLocationType.icon); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -1630,7 +1632,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements return; } - if (viewModel.getLoyaltyCard().imageThumbnail == null) { + if (viewModel.getLoyaltyCard().getImageThumbnail(this) == null) { thumbnail.setBackgroundColor(headerColor); LetterBitmap letterBitmap = Utils.generateIcon(this, store, headerColor); diff --git a/app/src/main/java/protect/card_locker/PkpassParser.kt b/app/src/main/java/protect/card_locker/PkpassParser.kt index d3c325826..313c3bb35 100644 --- a/app/src/main/java/protect/card_locker/PkpassParser.kt +++ b/app/src/main/java/protect/card_locker/PkpassParser.kt @@ -128,6 +128,9 @@ class PkpassParser(context: Context, uri: Uri?) { archiveStatus, image, null, + null, + null, + null, null ) } 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 c56e75666..6803e3cf2 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java @@ -69,7 +69,7 @@ public class CatimaExporter implements Exporter { // For each image for (ImageLocationType imageLocationType : ImageLocationType.values()) { // If it exists, add to the .zip file - Bitmap image = card.getImageForImageLocationType(imageLocationType); + Bitmap image = card.getImageForImageLocationType(context, imageLocationType); if (image != null) { ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password); zipOutputStream.putNextEntry(imageZipParameters); 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 08e3f97a7..6a0e215bd 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -152,7 +152,7 @@ public class CatimaImporter implements Importer { } public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card, final Set existingImages, final Map imageChecksums) throws IOException { - if (!LoyaltyCard.isDuplicate(existing, card)) { + if (!LoyaltyCard.isDuplicate(context, existing, card)) { return false; } for (ImageLocationType imageLocationType : ImageLocationType.values()) { @@ -508,6 +508,9 @@ public class CatimaImporter implements Importer { archiveStatus, null, null, + null, + 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 e25a41d16..8f711d0d6 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -167,6 +167,9 @@ public class FidmeImporter implements Importer { archiveStatus, null, null, + null, + null, + null, null ); } 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 c022696bb..1626b41d3 100644 --- a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java @@ -372,6 +372,9 @@ public class StocardImporter implements Importer { 0, null, null, + null, + null, + null, null ); importedData.cards.add(card); 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 94c7ba5e8..c86186320 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -169,6 +169,9 @@ public class VoucherVaultImporter implements Importer { 0, null, null, + null, + null, + null, null )); } diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index 07f9af1bd..2fc8d6121 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -307,7 +307,12 @@ public class LoyaltyCardViewActivityTest { } catch (ClassCastException e) { // This is probably a VectorDrawable, the placeholder image. Aka: No image. } - assertEquals(contents, image); + + if (contents == null && image == null) { + return; + } + + assertTrue(image.sameAs((Bitmap) contents)); } else { throw new UnsupportedOperationException(); } From 8009baca2632fe45d777004d3d6a19ef2b837341 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 5 Dec 2024 18:39:25 +0100 Subject: [PATCH 11/11] Remove unnecessary image load from storage calls The LoyaltyCard object itself loads the images itself --- .../protect/card_locker/CardsOnPowerScreenService.java | 2 +- .../protect/card_locker/LoyaltyCardCursorAdapter.java | 2 +- .../java/protect/card_locker/LoyaltyCardViewActivity.java | 8 ++++---- app/src/main/java/protect/card_locker/ShortcutHelper.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java index b010b4b79..46deaf467 100644 --- a/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java +++ b/app/src/main/java/protect/card_locker/CardsOnPowerScreenService.java @@ -99,7 +99,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService { } private Bitmap getIcon(Context context, LoyaltyCard loyaltyCard) { - Bitmap cardIcon = Utils.retrieveCardImage(context, loyaltyCard.id, ImageLocationType.icon); + Bitmap cardIcon = loyaltyCard.getImageThumbnail(context); if (cardIcon != null) { return cardIcon; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index 446df08bf..fa4f7fb93 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -89,7 +89,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter showBalanceUpdateDialog()); binding.iconContainer.setOnClickListener(view -> { - if (Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.icon) != null) { + if (loyaltyCard.getImageThumbnail(this) != null) { openImageInGallery(ImageType.ICON); } else { Toast.makeText(LoyaltyCardViewActivity.this, R.string.icon_header_click_text, Toast.LENGTH_LONG).show(); @@ -719,7 +719,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements editButtonIcon.setTint(Utils.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE); binding.fabEdit.setImageDrawable(editButtonIcon); - Bitmap icon = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.icon); + Bitmap icon = loyaltyCard.getImageThumbnail(this); Utils.setIconOrTextWithBackground(this, loyaltyCard, icon, binding.iconImage, binding.iconText, 1); // If the background is very bright, we should use dark icons @@ -748,12 +748,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements imageTypes.add(ImageType.BARCODE); } - frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.front); + frontImageBitmap = loyaltyCard.getImageFront(this); if (frontImageBitmap != null) { imageTypes.add(ImageType.IMAGE_FRONT); } - backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.back); + backImageBitmap = loyaltyCard.getImageBack(this); if (backImageBitmap != null) { imageTypes.add(ImageType.IMAGE_BACK); } diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index 9b93e9598..f57ae7c52 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -135,7 +135,7 @@ class ShortcutHelper { bundle.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id); intent.putExtras(bundle); - Bitmap iconBitmap = Utils.retrieveCardImage(context, loyaltyCard.id, ImageLocationType.icon); + Bitmap iconBitmap = loyaltyCard.getImageThumbnail(context); if (iconBitmap == null) { iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile(); } else {