From 940be028519722a818e757fb1bfae675bb99afef Mon Sep 17 00:00:00 2001 From: Miha Frange? Date: Mon, 14 Oct 2019 14:42:54 +0200 Subject: [PATCH 01/77] Added option to keep the screen on --- .../card_locker/LoyaltyCardViewActivity.java | 16 ++++++++++++++-- .../card_locker/preferences/Settings.java | 5 +++++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/preferences.xml | 6 ++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 3d103c5f5..3ab695250 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -233,10 +233,22 @@ public class LoyaltyCardViewActivity extends AppCompatActivity // '1' is the brightest. We attempt to maximize the brightness // to help barcode readers scan the barcode. Window window = getWindow(); - if(window != null && settings.useMaxBrightnessDisplayingBarcode()) + if(window != null) { WindowManager.LayoutParams attributes = window.getAttributes(); - attributes.screenBrightness = 1F; + + if (settings.useMaxBrightnessDisplayingBarcode()) + { + attributes.screenBrightness = 1F; + } + + if (settings.getKeepScreenOn()) + { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON| + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD| + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + window.setAttributes(attributes); } diff --git a/app/src/main/java/protect/card_locker/preferences/Settings.java b/app/src/main/java/protect/card_locker/preferences/Settings.java index b6808f352..7cb0aa715 100644 --- a/app/src/main/java/protect/card_locker/preferences/Settings.java +++ b/app/src/main/java/protect/card_locker/preferences/Settings.java @@ -90,4 +90,9 @@ public class Settings { return getBoolean(R.string.settings_key_lock_barcode_orientation, false); } + + public boolean getKeepScreenOn() + { + return getBoolean(R.string.settings_key_keep_screen_on, false); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0f2510d8..20dbf76bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,6 +104,8 @@ pref_display_card_max_brightness Lock barcode orientation pref_lock_barcode_orientation + Keep screen on + pref_keep_screen_on sharedpreference_active_tab diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b176a17a7..654949865 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -58,6 +58,12 @@ android:key="@string/settings_key_lock_barcode_orientation" android:title="@string/settings_lock_barcode_orientation" app:iconSpaceReserved="false" /> + + \ No newline at end of file From 2d6dfdcfdf3bd58866156376f78ae26c87c4a248 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 14 Feb 2021 23:32:07 +0100 Subject: [PATCH 02/77] Initial balance support --- CHANGELOG.md | 6 + .../card_locker/CsvDatabaseExporter.java | 4 + .../card_locker/CsvDatabaseImporter.java | 14 +- .../java/protect/card_locker/DBHelper.java | 29 ++- .../protect/card_locker/ImportURIHelper.java | 18 +- .../java/protect/card_locker/LoyaltyCard.java | 18 +- .../card_locker/LoyaltyCardCursorAdapter.java | 11 ++ .../card_locker/LoyaltyCardEditActivity.java | 171 ++++++++++++++++-- .../card_locker/LoyaltyCardViewActivity.java | 14 +- .../main/java/protect/card_locker/Utils.java | 42 +++++ .../res/layout/loyalty_card_edit_activity.xml | 51 +++++- .../main/res/layout/loyalty_card_layout.xml | 8 + .../res/layout/loyalty_card_view_layout.xml | 11 ++ app/src/main/res/values/strings.xml | 7 + 14 files changed, 380 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc08c7ec..9e1c65227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +Changes: + +- Add balance support + ## v1.8.1 (2021-02-12) Changes: diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java index a4c8fdb28..dd8a3037c 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java +++ b/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java @@ -50,6 +50,8 @@ public class CsvDatabaseExporter implements DatabaseExporter DBHelper.LoyaltyCardDbIds.STORE, DBHelper.LoyaltyCardDbIds.NOTE, DBHelper.LoyaltyCardDbIds.EXPIRY, + DBHelper.LoyaltyCardDbIds.BALANCE, + DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, DBHelper.LoyaltyCardDbIds.CARD_ID, DBHelper.LoyaltyCardDbIds.HEADER_COLOR, DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, @@ -65,6 +67,8 @@ public class CsvDatabaseExporter implements DatabaseExporter card.store, card.note, card.expiry != null ? card.expiry.getTime() : "", + card.balance, + card.balanceType, card.cardId, card.headerColor, card.barcodeType, diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java index 5040ddfcd..c7aae8ef2 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java @@ -10,6 +10,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; +import java.math.BigDecimal; import java.util.Date; import java.util.List; @@ -301,6 +302,17 @@ public class CsvDatabaseImporter implements DatabaseImporter expiry = new Date(extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true)); } catch (NullPointerException | FormatException e) { } + BigDecimal balance; + String balanceType = null; + try { + balance = new BigDecimal(extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null)); + balanceType = extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, null); + } catch (FormatException _e ) { + // These fields did not exist in versions 1.8.1 and before + // We catch this exception so we can still import old backups + balance = new BigDecimal("0.0"); + } + String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); if(cardId.isEmpty()) { @@ -324,7 +336,7 @@ public class CsvDatabaseImporter implements DatabaseImporter // We catch this exception so we can still import old backups } if (starStatus != 1) starStatus = 0; - helper.insertLoyaltyCard(database, id, store, note, expiry, cardId, barcodeType, headerColor, starStatus); + helper.insertLoyaltyCard(database, id, store, note, expiry, balance, balanceType, cardId, barcodeType, headerColor, starStatus); } /** diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 1c91b48e6..402585ee2 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -8,6 +8,7 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Color; +import java.math.BigDecimal; import java.util.Date; import java.util.ArrayList; import java.util.List; @@ -16,7 +17,7 @@ public class DBHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "Catima.db"; public static final int ORIGINAL_DATABASE_VERSION = 1; - public static final int DATABASE_VERSION = 7; + public static final int DATABASE_VERSION = 8; static class LoyaltyCardDbGroups { @@ -31,6 +32,8 @@ public class DBHelper extends SQLiteOpenHelper public static final String ID = "_id"; public static final String STORE = "store"; public static final String EXPIRY = "expiry"; + public static final String BALANCE = "balance"; + public static final String BALANCE_TYPE = "balance_type"; public static final String NOTE = "note"; public static final String HEADER_COLOR = "headercolor"; public static final String HEADER_TEXT_COLOR = "headertextcolor"; @@ -60,11 +63,14 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0')"); // create table for cards + // Balance is TEXT and not REAL to be able to store a BigDecimal without precision loss db.execSQL("create table " + LoyaltyCardDbIds.TABLE + "(" + LoyaltyCardDbIds.ID + " INTEGER primary key autoincrement," + LoyaltyCardDbIds.STORE + " TEXT not null," + LoyaltyCardDbIds.NOTE + " TEXT not null," + LoyaltyCardDbIds.EXPIRY + " INTEGER," + + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0.0'," + + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT," + LoyaltyCardDbIds.HEADER_COLOR + " INTEGER," + LoyaltyCardDbIds.HEADER_TEXT_COLOR + " INTEGER," + LoyaltyCardDbIds.CARD_ID + " TEXT not null," + @@ -128,9 +134,18 @@ public class DBHelper extends SQLiteOpenHelper db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.EXPIRY + " INTEGER"); } + + if(oldVersion < 8 && newVersion >= 8) + { + db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0.0'"); + db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT"); + } } public long insertLoyaltyCard(final String store, final String note, final Date expiry, + final BigDecimal balance, final String balanceType, final String cardId, final String barcodeType, final Integer headerColor, final int starStatus) { @@ -139,6 +154,8 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.STORE, store); contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); + contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); @@ -149,7 +166,8 @@ public class DBHelper extends SQLiteOpenHelper } public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store, - final String note, final Date expiry, final String cardId, + final String note, final Date expiry, final BigDecimal balance, + final String balanceType, final String cardId, final String barcodeType, final Integer headerColor, final int starStatus) { @@ -158,6 +176,8 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.STORE, store); contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); + contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); @@ -168,7 +188,8 @@ public class DBHelper extends SQLiteOpenHelper } public boolean updateLoyaltyCard(final int id, final String store, final String note, - final Date expiry, final String cardId, + final Date expiry, final BigDecimal balance, + final String balanceType, final String cardId, final String barcodeType, final Integer headerColor) { SQLiteDatabase db = getWritableDatabase(); @@ -176,6 +197,8 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.STORE, store); contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); + contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index ba77d83cf..9e276e630 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -4,12 +4,15 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import java.io.InvalidObjectException; +import java.math.BigDecimal; import java.util.Date; public class ImportURIHelper { private static final String STORE = DBHelper.LoyaltyCardDbIds.STORE; private static final String NOTE = DBHelper.LoyaltyCardDbIds.NOTE; private static final String EXPIRY = DBHelper.LoyaltyCardDbIds.EXPIRY; + private static final String BALANCE = DBHelper.LoyaltyCardDbIds.BALANCE; + private static final String BALANCE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE; private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID; private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE; @@ -43,6 +46,8 @@ public class ImportURIHelper { try { // These values are allowed to be null Date expiry = null; + BigDecimal balance = new BigDecimal("0.0"); + String balanceType = null; Integer headerColor = null; Integer headerTextColor = null; @@ -57,13 +62,24 @@ public class ImportURIHelper { { expiry = new Date(Long.parseLong(unparsedExpiry)); } + String unparsedBalance = uri.getQueryParameter(BALANCE); + if(unparsedBalance != null && !unparsedBalance.equals("")) + { + balance = new BigDecimal(unparsedBalance); + } + String unparsedBalanceType = uri.getQueryParameter(BALANCE_TYPE); + if (unparsedBalanceType != null && !unparsedBalanceType.equals("")) + { + balanceType = unparsedBalanceType; + } + String unparsedHeaderColor = uri.getQueryParameter(HEADER_COLOR); if(unparsedHeaderColor != null) { headerColor = Integer.parseInt(unparsedHeaderColor); } - return new LoyaltyCard(-1, store, note, expiry, cardId, barcodeType, headerColor, headerTextColor, 0); + return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeType, headerColor, headerTextColor, 0); } catch (NullPointerException | NumberFormatException 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 b8dcaf1d8..39e22f05a 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -2,7 +2,8 @@ package protect.card_locker; import android.database.Cursor; -import java.text.DateFormat; +import java.math.BigDecimal; +import java.util.Currency; import java.util.Date; import androidx.annotation.Nullable; @@ -13,6 +14,8 @@ public class LoyaltyCard public final String store; public final String note; public final Date expiry; + public final BigDecimal balance; + public final Currency balanceType; public final String cardId; public final String barcodeType; @@ -24,7 +27,8 @@ public class LoyaltyCard public final int starStatus; - public LoyaltyCard(final int id, final String store, final String note, final Date expiry, final String cardId, + public LoyaltyCard(final int id, final String store, final String note, final Date expiry, + final BigDecimal balance, final String balanceType, final String cardId, final String barcodeType, final Integer headerColor, final Integer headerTextColor, final int starStatus) { @@ -32,6 +36,12 @@ public class LoyaltyCard this.store = store; this.note = note; this.expiry = expiry; + this.balance = balance; + if (balanceType != null) { + this.balanceType = Currency.getInstance(balanceType); + } else { + this.balanceType = null; + } this.cardId = cardId; this.barcodeType = barcodeType; this.headerColor = headerColor; @@ -45,6 +55,8 @@ public class LoyaltyCard String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE)); String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE)); long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY)); + BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); + String balanceType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE)); String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID)); String barcodeType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE)); int starred = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS)); @@ -71,6 +83,6 @@ public class LoyaltyCard headerTextColor = cursor.getInt(headerTextColorColumn); } - return new LoyaltyCard(id, store, note, expiry, cardId, barcodeType, headerColor, headerTextColor, starred); + return new LoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeType, headerColor, headerTextColor, starred); } } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index fa7568547..4f069c5ed 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -9,6 +9,7 @@ import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.TextView; +import java.math.BigDecimal; import java.text.DateFormat; import java.util.Date; @@ -41,6 +42,7 @@ class LoyaltyCardCursorAdapter extends CursorAdapter ImageView thumbnail = view.findViewById(R.id.thumbnail); TextView storeField = view.findViewById(R.id.store); TextView noteField = view.findViewById(R.id.note); + TextView balanceField = view.findViewById(R.id.balance); TextView expiryField = view.findViewById(R.id.expiry); ImageView star = view.findViewById(R.id.star); @@ -63,6 +65,15 @@ class LoyaltyCardCursorAdapter extends CursorAdapter noteField.setVisibility(View.GONE); } + if(!loyaltyCard.balance.equals(new BigDecimal(0))) { + balanceField.setVisibility(View.VISIBLE); + balanceField.setText(context.getString(R.string.balanceSentence, Utils.formatBalance(loyaltyCard.balance, loyaltyCard.balanceType))); + } + else + { + balanceField.setVisibility(View.GONE); + } + if(loyaltyCard.expiry != null) { expiryField.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 3e3689113..61202eb4f 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -1,5 +1,6 @@ package protect.card_locker; +import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.app.Dialog; import android.content.DialogInterface; @@ -7,6 +8,7 @@ import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import com.google.android.material.chip.Chip; @@ -19,6 +21,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.DialogFragment; +import android.os.LocaleList; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -45,11 +48,21 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; import java.io.InvalidObjectException; +import java.math.BigDecimal; import java.text.DateFormat; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Currency; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Locale; public class LoyaltyCardEditActivity extends AppCompatActivity { @@ -62,6 +75,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity EditText noteFieldEdit; ChipGroup groupsChips; AutoCompleteTextView expiryField; + EditText balanceField; + AutoCompleteTextView balanceCurrencyField; View cardAndBarcodeLayout; TextView cardIdFieldView; AutoCompleteTextView barcodeTypeField; @@ -86,6 +101,10 @@ public class LoyaltyCardEditActivity extends AppCompatActivity boolean initDone = false; AlertDialog confirmExitDialog = null; + boolean validBalance = true; + + HashMap currencies = new HashMap<>(); + private void extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); @@ -120,12 +139,18 @@ public class LoyaltyCardEditActivity extends AppCompatActivity db = new DBHelper(this); importUriHelper = new ImportURIHelper(this); + for (Currency currency : Currency.getAvailableCurrencies()) { + currencies.put(currency.getSymbol(), currency); + } + tabs = findViewById(R.id.tabs); thumbnail = findViewById(R.id.thumbnail); storeFieldEdit = findViewById(R.id.storeNameEdit); noteFieldEdit = findViewById(R.id.noteEdit); groupsChips = findViewById(R.id.groupChips); expiryField = findViewById(R.id.expiryField); + balanceField = findViewById(R.id.balanceField); + balanceCurrencyField = findViewById(R.id.balanceCurrencyField); cardAndBarcodeLayout = findViewById(R.id.cardAndBarcodeLayout); cardIdFieldView = findViewById(R.id.cardIdView); barcodeTypeField = findViewById(R.id.barcodeTypeField); @@ -183,6 +208,88 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } }); + balanceField.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + try { + balanceField.setTag(Utils.parseCurrencyInUserLocale(s.toString())); + validBalance = true; + } catch (ParseException | NumberFormatException e) { + validBalance = false; + e.printStackTrace(); + } + } + + @Override + public void afterTextChanged(Editable s) { } + }); + + balanceCurrencyField.addTextChangedListener(new TextWatcher() { + CharSequence lastValue; + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + lastValue = s; + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + hasChanged = true; + + Currency currency; + + if (s.toString().equals(getString(R.string.points))) { + currency = null; + } else { + currency = currencies.get(s.toString()); + } + + balanceCurrencyField.setTag(currency); + + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol((BigDecimal) balanceField.getTag(), currency)); + } + + @Override + public void afterTextChanged(Editable s) { + ArrayList currencyList = new ArrayList<>(currencies.keySet()); + Collections.sort(currencyList, (o1, o2) -> { + boolean o1ascii = o1.matches("^[^a-zA-Z]*$"); + boolean o2ascii = o2.matches("^[^a-zA-Z]*$"); + + if (!o1ascii && o2ascii) { + return 1; + } else if (o1ascii && !o2ascii) { + return -1; + } + + return o1.compareTo(o2); + }); + + // Sort locale currencies on top + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList locales = getApplicationContext().getResources().getConfiguration().getLocales(); + + for (int i = locales.size() - 1; i > 0; i--) { + Locale locale = locales.get(i); + String currencySymbol = Currency.getInstance(locale).getSymbol(); + currencyList.remove(currencySymbol); + currencyList.add(0, currencySymbol); + } + } else { + String currencySymbol = Currency.getInstance(getApplicationContext().getResources().getConfiguration().locale).getSymbol(); + currencyList.remove(currencySymbol); + currencyList.add(0, currencySymbol); + } + + currencyList.add(0, getString(R.string.points)); + ArrayAdapter currencyAdapter = new ArrayAdapter<>(LoyaltyCardEditActivity.this, android.R.layout.select_dialog_item, currencyList); + balanceCurrencyField.setAdapter(currencyAdapter); + } + }); + cardIdFieldView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @@ -262,10 +369,15 @@ public class LoyaltyCardEditActivity extends AppCompatActivity noteFieldEdit.setText(""); expiryField.setTag(null); expiryField.setText(""); + balanceField.setTag(null); + balanceField.setText(""); + balanceCurrencyField.setTag(null); + balanceCurrencyField.setText(""); cardIdFieldView.setText(""); barcodeTypeField.setText(""); } + @SuppressLint("DefaultLocale") @Override public void onResume() { @@ -297,11 +409,19 @@ public class LoyaltyCardEditActivity extends AppCompatActivity if(expiryField.getText().length() == 0) { expiryField.setTag(loyaltyCard.expiry); - if (loyaltyCard.expiry == null) { - expiryField.setText(getString(R.string.never)); - } else { - expiryField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry)); - } + formatExpiryField(loyaltyCard.expiry); + } + + if(balanceField.getText().length() == 0) + { + balanceField.setTag(loyaltyCard.balance); + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(loyaltyCard.balance, loyaltyCard.balanceType)); + } + + if(balanceCurrencyField.getText().length() == 0) + { + balanceCurrencyField.setTag(loyaltyCard.balanceType); + formatBalanceCurrencyField(loyaltyCard.balanceType); } if(cardIdFieldView.getText().length() == 0) @@ -340,11 +460,9 @@ public class LoyaltyCardEditActivity extends AppCompatActivity storeFieldEdit.setText(importCard.store); noteFieldEdit.setText(importCard.note); expiryField.setTag(importCard.expiry); - if (importCard.expiry != null) { - expiryField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(importCard.expiry)); - } else { - expiryField.setText(R.string.never); - } + formatExpiryField(importCard.expiry); + balanceField.setTag(importCard.balanceType); + formatBalanceCurrencyField(importCard.balanceType); cardIdFieldView.setText(importCard.cardId); barcodeTypeField.setText(importCard.barcodeType); headingColorValue = importCard.headerColor; @@ -354,6 +472,10 @@ public class LoyaltyCardEditActivity extends AppCompatActivity setTitle(R.string.addCardTitle); expiryField.setTag(null); expiryField.setText(getString(R.string.never)); + balanceField.setTag(null); + balanceField.setText(String.format("%f", new BigDecimal("0.0"))); + balanceCurrencyField.setTag(null); + balanceCurrencyField.setText(getString(R.string.points)); hideBarcode(); } @@ -456,6 +578,22 @@ public class LoyaltyCardEditActivity extends AppCompatActivity generateIcon(storeFieldEdit.getText().toString()); } + private void formatExpiryField(Date expiry) { + if (expiry == null) { + expiryField.setText(getString(R.string.never)); + } else { + expiryField.setText(DateFormat.getDateInstance(DateFormat.LONG).format(expiry)); + } + } + + private void formatBalanceCurrencyField(Currency balanceType) { + if (balanceType == null) { + balanceCurrencyField.setText(getString(R.string.points)); + } else { + balanceCurrencyField.setText(balanceType.getSymbol()); + } + } + @Override public void onBackPressed() { askBeforeQuitIfChanged(); @@ -584,12 +722,13 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } } - private void doSave() { String store = storeFieldEdit.getText().toString(); String note = noteFieldEdit.getText().toString(); Date expiry = (Date) expiryField.getTag(); + BigDecimal balance = (BigDecimal) balanceField.getTag(); + String balanceType = balanceCurrencyField.getTag() != null ? ((Currency) balanceCurrencyField.getTag()).getCurrencyCode() : null; String cardId = cardIdFieldView.getText().toString(); String barcodeType = barcodeTypeField.getText().toString(); @@ -612,6 +751,12 @@ public class LoyaltyCardEditActivity extends AppCompatActivity return; } + if(!validBalance) + { + Snackbar.make(balanceField, getString(R.string.parsingBalanceFailed, balanceField.getText().toString()), Snackbar.LENGTH_LONG).show(); + return; + } + List selectedGroups = new ArrayList<>(); for (Integer chipId : groupsChips.getCheckedChipIds()) { @@ -621,12 +766,12 @@ public class LoyaltyCardEditActivity extends AppCompatActivity if(updateLoyaltyCard) { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) - db.updateLoyaltyCard(loyaltyCardId, store, note, expiry, cardId, barcodeType, headingColorValue); + db.updateLoyaltyCard(loyaltyCardId, store, note, expiry, balance, balanceType, cardId, barcodeType, headingColorValue); Log.i(TAG, "Updated " + loyaltyCardId + " to " + cardId); } else { - loyaltyCardId = (int)db.insertLoyaltyCard(store, note, expiry, cardId, barcodeType, headingColorValue, 0); + loyaltyCardId = (int)db.insertLoyaltyCard(store, note, expiry, balance, balanceType, cardId, barcodeType, headingColorValue, 0); } db.setLoyaltyCardGroups(loyaltyCardId, selectedGroups); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 3d103c5f5..c1e7ae719 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -39,6 +39,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.zxing.BarcodeFormat; +import java.math.BigDecimal; import java.text.DateFormat; import java.util.List; @@ -54,6 +55,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity ImageView bottomSheetButton; TextView noteView; TextView groupsView; + TextView balanceView; TextView expiryView; TextView storeName; ImageButton maximizeButton; @@ -123,6 +125,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity bottomSheetButton = findViewById(R.id.bottomSheetButton); noteView = findViewById(R.id.noteView); groupsView = findViewById(R.id.groupsView); + balanceView = findViewById(R.id.balanceView); expiryView = findViewById(R.id.expiryView); storeName = findViewById(R.id.storeName); maximizeButton = findViewById(R.id.maximizeButton); @@ -287,6 +290,15 @@ public class LoyaltyCardViewActivity extends AppCompatActivity groupsView.setVisibility(View.GONE); } + if(!loyaltyCard.balance.equals(new BigDecimal(0))) { + balanceView.setVisibility(View.VISIBLE); + balanceView.setText(getString(R.string.balanceSentence, Utils.formatBalance(loyaltyCard.balance, loyaltyCard.balanceType))); + } + else + { + balanceView.setVisibility(View.GONE); + } + if(loyaltyCard.expiry != null) { expiryView.setVisibility(View.VISIBLE); @@ -523,7 +535,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity private void makeBottomSheetVisibleIfUseful() { - if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) { + if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) { bottomSheet.setVisibility(View.VISIBLE); } else diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 3d0ed569f..49335ec29 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -6,9 +6,14 @@ import android.content.Intent; import android.graphics.Color; import android.util.Log; +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.Calendar; +import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; +import java.util.Locale; import androidx.core.graphics.ColorUtils; @@ -77,4 +82,41 @@ public class Utils { return expiryDate.before(date.getTime()); } + + static public String formatBalance(BigDecimal value, Currency currency) { + NumberFormat numberFormat = NumberFormat.getInstance(); + + if (currency == null) { + numberFormat.setMaximumFractionDigits(0); + return numberFormat.format(value); + } + + NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(); + currencyFormat.setCurrency(currency); + + return currencyFormat.format(value); + } + + static public String formatBalanceWithoutCurrencySymbol(BigDecimal value, Currency currency) { + NumberFormat numberFormat = NumberFormat.getInstance(); + + if (currency == null) { + numberFormat.setMaximumFractionDigits(0); + return numberFormat.format(value); + } + + numberFormat.setMinimumFractionDigits(currency.getDefaultFractionDigits()); + numberFormat.setMaximumFractionDigits(currency.getDefaultFractionDigits()); + + return numberFormat.format(value); + } + + static public BigDecimal parseCurrencyInUserLocale(String value) throws ParseException, NumberFormatException { + // BigDecimal only likes to parse in US locale + // So we have to translate whatever the input was to US locale + NumberFormat numberInputFormat = NumberFormat.getNumberInstance(); + NumberFormat numberToBigDecimalFormat = NumberFormat.getNumberInstance(Locale.US); + + return new BigDecimal(numberToBigDecimalFormat.format(numberInputFormat.parse(value))); + } } diff --git a/app/src/main/res/layout/loyalty_card_edit_activity.xml b/app/src/main/res/layout/loyalty_card_edit_activity.xml index 7e02fb469..ae421711c 100644 --- a/app/src/main/res/layout/loyalty_card_edit_activity.xml +++ b/app/src/main/res/layout/loyalty_card_edit_activity.xml @@ -136,6 +136,53 @@ android:textSize="@dimen/inputSize" /> + + + + + + + + + + + + + + + + + + + - + android:hint="@string/cardId" + android:labelFor="@+id/cardIdView"> + + + + Groups: %s Expires: %s Expired: %s + Balance: %s Card Barcode @@ -143,4 +144,10 @@ Choose expiry date Move the barcode to the top of the screen Center the barcode on the screen + + Balance + Currency + Points + + %s does not seem to be a valid balance. From 1675ebf1dc3b1dd95a73681e24adf7f526215d61 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 15 Feb 2021 21:58:22 +0100 Subject: [PATCH 03/77] Set hasChanged when balance was edited --- .../main/java/protect/card_locker/LoyaltyCardEditActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 61202eb4f..6148b4d01 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -214,6 +214,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + hasChanged = true; + try { balanceField.setTag(Utils.parseCurrencyInUserLocale(s.toString())); validBalance = true; From 4fbedbc30c7faaaf65943f00e07d118baf1ede83 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 15 Feb 2021 22:02:47 +0100 Subject: [PATCH 04/77] Fix parsing big numbers --- app/src/main/java/protect/card_locker/Utils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 49335ec29..6013e4619 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -117,6 +117,9 @@ public class Utils { NumberFormat numberInputFormat = NumberFormat.getNumberInstance(); NumberFormat numberToBigDecimalFormat = NumberFormat.getNumberInstance(Locale.US); + // BigDecimal won't understand values like 1,000 instead of 1000 + numberToBigDecimalFormat.setGroupingUsed(false); + return new BigDecimal(numberToBigDecimalFormat.format(numberInputFormat.parse(value))); } } From 1dc30ebaad277875e6a271e566bfa358b13b06a5 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 15 Feb 2021 22:30:45 +0100 Subject: [PATCH 05/77] Reformat balance after focus loss --- .../card_locker/LoyaltyCardEditActivity.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 6148b4d01..f94a9ca1f 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -208,6 +208,12 @@ public class LoyaltyCardEditActivity extends AppCompatActivity } }); + balanceField.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol((BigDecimal) balanceField.getTag(), (Currency) balanceCurrencyField.getTag())); + } + }); + balanceField.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @@ -217,8 +223,10 @@ public class LoyaltyCardEditActivity extends AppCompatActivity hasChanged = true; try { - balanceField.setTag(Utils.parseCurrencyInUserLocale(s.toString())); + BigDecimal balance = Utils.parseCurrencyInUserLocale(s.toString()); validBalance = true; + + balanceField.setTag(balance); } catch (ParseException | NumberFormatException e) { validBalance = false; e.printStackTrace(); @@ -230,12 +238,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity }); balanceCurrencyField.addTextChangedListener(new TextWatcher() { - CharSequence lastValue; - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - lastValue = s; - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { From 4a568d02d38b6ad4ebd27519f020593e0e154fbc Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 16 Feb 2021 22:22:51 +0100 Subject: [PATCH 06/77] Various fixes and consistency improvements --- .../card_locker/CsvDatabaseImporter.java | 9 ++++++-- .../java/protect/card_locker/DBHelper.java | 15 ++++++------- .../protect/card_locker/ImportURIHelper.java | 21 ++++++++++++------- .../java/protect/card_locker/LoyaltyCard.java | 16 +++++++------- .../card_locker/LoyaltyCardEditActivity.java | 10 ++++----- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java index c7aae8ef2..a4105002c 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; +import java.util.Currency; import java.util.Date; import java.util.List; @@ -303,16 +304,20 @@ public class CsvDatabaseImporter implements DatabaseImporter } catch (NullPointerException | FormatException e) { } BigDecimal balance; - String balanceType = null; try { balance = new BigDecimal(extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null)); - balanceType = extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, null); } catch (FormatException _e ) { // These fields did not exist in versions 1.8.1 and before // We catch this exception so we can still import old backups balance = new BigDecimal("0.0"); } + Currency balanceType = null; + String unparsedBalanceType = extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, ""); + if(!unparsedBalanceType.isEmpty()) { + balanceType = Currency.getInstance(unparsedBalanceType); + } + String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); if(cardId.isEmpty()) { diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 402585ee2..d25de0cb3 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -9,6 +9,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Color; import java.math.BigDecimal; +import java.util.Currency; import java.util.Date; import java.util.ArrayList; import java.util.List; @@ -33,7 +34,7 @@ public class DBHelper extends SQLiteOpenHelper public static final String STORE = "store"; public static final String EXPIRY = "expiry"; public static final String BALANCE = "balance"; - public static final String BALANCE_TYPE = "balance_type"; + public static final String BALANCE_TYPE = "balancetype"; public static final String NOTE = "note"; public static final String HEADER_COLOR = "headercolor"; public static final String HEADER_TEXT_COLOR = "headertextcolor"; @@ -145,7 +146,7 @@ public class DBHelper extends SQLiteOpenHelper } public long insertLoyaltyCard(final String store, final String note, final Date expiry, - final BigDecimal balance, final String balanceType, + final BigDecimal balance, final Currency balanceType, final String cardId, final String barcodeType, final Integer headerColor, final int starStatus) { @@ -155,7 +156,7 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); - contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); @@ -167,7 +168,7 @@ public class DBHelper extends SQLiteOpenHelper public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store, final String note, final Date expiry, final BigDecimal balance, - final String balanceType, final String cardId, + final Currency balanceType, final String cardId, final String barcodeType, final Integer headerColor, final int starStatus) { @@ -177,7 +178,7 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); - contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); @@ -189,7 +190,7 @@ public class DBHelper extends SQLiteOpenHelper public boolean updateLoyaltyCard(final int id, final String store, final String note, final Date expiry, final BigDecimal balance, - final String balanceType, final String cardId, + final Currency balanceType, final String cardId, final String barcodeType, final Integer headerColor) { SQLiteDatabase db = getWritableDatabase(); @@ -198,7 +199,7 @@ public class DBHelper extends SQLiteOpenHelper contentValues.put(LoyaltyCardDbIds.NOTE, note); contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); - contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null); contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index 9e276e630..776f44832 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.net.Uri; import java.io.InvalidObjectException; import java.math.BigDecimal; +import java.util.Currency; import java.util.Date; public class ImportURIHelper { @@ -12,7 +13,7 @@ public class ImportURIHelper { private static final String NOTE = DBHelper.LoyaltyCardDbIds.NOTE; private static final String EXPIRY = DBHelper.LoyaltyCardDbIds.EXPIRY; private static final String BALANCE = DBHelper.LoyaltyCardDbIds.BALANCE; - private static final String BALANCE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE; + private static final String BALANCE_TYPE = DBHelper.LoyaltyCardDbIds.BALANCE_TYPE; private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID; private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE; @@ -47,7 +48,7 @@ public class ImportURIHelper { // These values are allowed to be null Date expiry = null; BigDecimal balance = new BigDecimal("0.0"); - String balanceType = null; + Currency balanceType = null; Integer headerColor = null; Integer headerTextColor = null; @@ -57,11 +58,6 @@ public class ImportURIHelper { String barcodeType = uri.getQueryParameter(BARCODE_TYPE); if (store == null || note == null || cardId == null || barcodeType == null) throw new InvalidObjectException("Not a valid import URI"); - String unparsedExpiry = uri.getQueryParameter(EXPIRY); - if(unparsedExpiry != null && !unparsedExpiry.equals("")) - { - expiry = new Date(Long.parseLong(unparsedExpiry)); - } String unparsedBalance = uri.getQueryParameter(BALANCE); if(unparsedBalance != null && !unparsedBalance.equals("")) { @@ -70,7 +66,12 @@ public class ImportURIHelper { String unparsedBalanceType = uri.getQueryParameter(BALANCE_TYPE); if (unparsedBalanceType != null && !unparsedBalanceType.equals("")) { - balanceType = unparsedBalanceType; + balanceType = Currency.getInstance(unparsedBalanceType); + } + String unparsedExpiry = uri.getQueryParameter(EXPIRY); + if(unparsedExpiry != null && !unparsedExpiry.equals("")) + { + expiry = new Date(Long.parseLong(unparsedExpiry)); } String unparsedHeaderColor = uri.getQueryParameter(HEADER_COLOR); @@ -93,6 +94,10 @@ public class ImportURIHelper { uriBuilder.path(path); uriBuilder.appendQueryParameter(STORE, loyaltyCard.store); uriBuilder.appendQueryParameter(NOTE, loyaltyCard.note); + uriBuilder.appendQueryParameter(BALANCE, loyaltyCard.balance.toString()); + if (loyaltyCard.balanceType != null) { + uriBuilder.appendQueryParameter(BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode()); + } if (loyaltyCard.expiry != null) { uriBuilder.appendQueryParameter(EXPIRY, String.valueOf(loyaltyCard.expiry.getTime())); } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 39e22f05a..64461a0e1 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -28,7 +28,7 @@ public class LoyaltyCard public final int starStatus; public LoyaltyCard(final int id, final String store, final String note, final Date expiry, - final BigDecimal balance, final String balanceType, final String cardId, + final BigDecimal balance, final Currency balanceType, final String cardId, final String barcodeType, final Integer headerColor, final Integer headerTextColor, final int starStatus) { @@ -37,11 +37,7 @@ public class LoyaltyCard this.note = note; this.expiry = expiry; this.balance = balance; - if (balanceType != null) { - this.balanceType = Currency.getInstance(balanceType); - } else { - this.balanceType = null; - } + this.balanceType = balanceType; this.cardId = cardId; this.barcodeType = barcodeType; this.headerColor = headerColor; @@ -56,18 +52,24 @@ public class LoyaltyCard String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE)); long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY)); BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); - String balanceType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE)); String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID)); String barcodeType = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE)); int starred = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS)); + int balanceTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE); int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR); int headerTextColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_TEXT_COLOR); + Currency balanceType = null; Date expiry = null; Integer headerColor = null; Integer headerTextColor = null; + if (cursor.isNull(balanceTypeColumn) == false) + { + balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn)); + } + if(expiryLong > 0) { expiry = new Date(expiryLong); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index f94a9ca1f..2d5970b55 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -467,7 +467,8 @@ public class LoyaltyCardEditActivity extends AppCompatActivity noteFieldEdit.setText(importCard.note); expiryField.setTag(importCard.expiry); formatExpiryField(importCard.expiry); - balanceField.setTag(importCard.balanceType); + balanceField.setTag(importCard.balance); + balanceCurrencyField.setTag(importCard.balanceType); formatBalanceCurrencyField(importCard.balanceType); cardIdFieldView.setText(importCard.cardId); barcodeTypeField.setText(importCard.barcodeType); @@ -478,10 +479,9 @@ public class LoyaltyCardEditActivity extends AppCompatActivity setTitle(R.string.addCardTitle); expiryField.setTag(null); expiryField.setText(getString(R.string.never)); - balanceField.setTag(null); - balanceField.setText(String.format("%f", new BigDecimal("0.0"))); + balanceField.setTag(new BigDecimal("0.0")); balanceCurrencyField.setTag(null); - balanceCurrencyField.setText(getString(R.string.points)); + formatBalanceCurrencyField(null); hideBarcode(); } @@ -734,7 +734,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity String note = noteFieldEdit.getText().toString(); Date expiry = (Date) expiryField.getTag(); BigDecimal balance = (BigDecimal) balanceField.getTag(); - String balanceType = balanceCurrencyField.getTag() != null ? ((Currency) balanceCurrencyField.getTag()).getCurrencyCode() : null; + Currency balanceType = balanceCurrencyField.getTag() != null ? ((Currency) balanceCurrencyField.getTag()) : null; String cardId = cardIdFieldView.getText().toString(); String barcodeType = barcodeTypeField.getText().toString(); From 0baf1ba348b58950dc7c3bf48def286cdb11e035 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 20 Feb 2021 18:15:58 +0100 Subject: [PATCH 07/77] Fix tests --- .../protect/card_locker/DatabaseTest.java | 23 ++++++------- .../protect/card_locker/ImportExportTest.java | 15 +++++---- .../protect/card_locker/ImportURITest.java | 5 +-- .../LoyaltyCardCursorAdapterTest.java | 13 ++++---- .../LoyaltyCardViewActivityTest.java | 33 ++++++++++--------- .../protect/card_locker/MainActivityTest.java | 15 +++++---- 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/app/src/test/java/protect/card_locker/DatabaseTest.java b/app/src/test/java/protect/card_locker/DatabaseTest.java index c8db55818..7fa31444a 100644 --- a/app/src/test/java/protect/card_locker/DatabaseTest.java +++ b/app/src/test/java/protect/card_locker/DatabaseTest.java @@ -15,6 +15,7 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -43,7 +44,7 @@ public class DatabaseTest public void addRemoveOneGiftCard() { assertEquals(0, db.getLoyaltyCardCount()); - long id = db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); + long id = db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -66,12 +67,12 @@ public class DatabaseTest @Test public void updateGiftCard() { - long id = db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); + long id = db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); - result = db.updateLoyaltyCard(1, "store1", "note1", null, "cardId1", BarcodeFormat.AZTEC.toString(), DEFAULT_HEADER_COLOR); + result = db.updateLoyaltyCard(1, "store1", "note1", null, new BigDecimal("0"), null, "cardId1", BarcodeFormat.AZTEC.toString(), DEFAULT_HEADER_COLOR); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -88,7 +89,7 @@ public class DatabaseTest @Test public void updateGiftCardOnlyStar() { - long id = db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); + long id = db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -112,7 +113,7 @@ public class DatabaseTest { assertEquals(0, db.getLoyaltyCardCount()); - boolean result = db.updateLoyaltyCard(1, "store1", "note1", null, "cardId1", + boolean result = db.updateLoyaltyCard(1, "store1", "note1", null, new BigDecimal("0"), null, "cardId1", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -121,7 +122,7 @@ public class DatabaseTest @Test public void emptyGiftCardValues() { - long id = db.insertLoyaltyCard("", "", null, "", "", null, 0); + long id = db.insertLoyaltyCard("", "", null, new BigDecimal("0"), null, "", "", null, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -144,7 +145,7 @@ public class DatabaseTest // that they are sorted for(int index = CARDS_TO_ADD-1; index >= 0; index--) { - long id = db.insertLoyaltyCard("store" + index, "note" + index, null, "cardId" + index, + long id = db.insertLoyaltyCard("store" + index, "note" + index, null, new BigDecimal("0"), null, "cardId" + index, BarcodeFormat.UPC_A.toString(), index, 0); boolean result = (id != -1); assertTrue(result); @@ -187,12 +188,12 @@ public class DatabaseTest for(int index = CARDS_TO_ADD-1; index >= 0; index--) { if (index == CARDS_TO_ADD-1) { - id = db.insertLoyaltyCard("store" + index, "note" + index, null, "cardId" + index, + id = db.insertLoyaltyCard("store" + index, "note" + index, null, new BigDecimal("0"), null, "cardId" + index, BarcodeFormat.UPC_A.toString(), index, 1); } else { - id = db.insertLoyaltyCard("store" + index, "note" + index, null, "cardId" + index, + id = db.insertLoyaltyCard("store" + index, "note" + index, null, new BigDecimal("0"), null, "cardId" + index, BarcodeFormat.UPC_A.toString(), index, 0); } boolean result = (id != -1); @@ -288,7 +289,7 @@ public class DatabaseTest { // Create card assertEquals(0, db.getLoyaltyCardCount()); - long id = db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); + long id = db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -405,7 +406,7 @@ public class DatabaseTest { // Create card assertEquals(0, db.getLoyaltyCardCount()); - long id = db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); + long id = db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), DEFAULT_HEADER_COLOR, 0); boolean result = (id != -1); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index ddf3b6c3f..831644377 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -24,6 +24,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; @@ -72,7 +73,7 @@ public class ImportExportTest { String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); - long id = db.insertLoyaltyCard(storeName, note, null, BARCODE_DATA, BARCODE_TYPE, index, 0); + long id = db.insertLoyaltyCard(storeName, note, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, BARCODE_TYPE, index, 0); boolean result = (id != -1); assertTrue(result); } @@ -88,7 +89,7 @@ public class ImportExportTest { String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); - long id = db.insertLoyaltyCard(storeName, note, null, BARCODE_DATA, BARCODE_TYPE, index, 1); + long id = db.insertLoyaltyCard(storeName, note, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, BARCODE_TYPE, index, 1); boolean result = (id != -1); assertTrue(result); } @@ -97,7 +98,7 @@ public class ImportExportTest String storeName = String.format("store, \"%4d", index); String note = String.format("note, \"%4d", index); //if index is even - long id = db.insertLoyaltyCard(storeName, note, null, BARCODE_DATA, BARCODE_TYPE, index, 0); + long id = db.insertLoyaltyCard(storeName, note, null, new BigDecimal(String.valueOf(index)), null, BARCODE_DATA, BARCODE_TYPE, index, 0); boolean result = (id != -1); assertTrue(result); } @@ -106,21 +107,21 @@ public class ImportExportTest private void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { - long id = db.insertLoyaltyCard("No Expiry", "", null, BARCODE_DATA, BARCODE_TYPE, 0, 0); + long id = db.insertLoyaltyCard("No Expiry", "", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); boolean result = (id != -1); assertTrue(result); - id = db.insertLoyaltyCard("Past", "", new Date((long) 1), BARCODE_DATA, BARCODE_TYPE, 0, 0); + id = db.insertLoyaltyCard("Past", "", new Date((long) 1), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); - id = db.insertLoyaltyCard("Today", "", new Date(), BARCODE_DATA, BARCODE_TYPE, 0, 0); + id = db.insertLoyaltyCard("Today", "", new Date(), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); // This will break after 19 January 2038 // If someone is still maintaining this code base by then: I love you - id = db.insertLoyaltyCard("Future", "", new Date(2147483648L), BARCODE_DATA, BARCODE_TYPE, 0, 0); + id = db.insertLoyaltyCard("Future", "", new Date(2147483648L), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); diff --git a/app/src/test/java/protect/card_locker/ImportURITest.java b/app/src/test/java/protect/card_locker/ImportURITest.java index 02bd27462..56b8153e8 100644 --- a/app/src/test/java/protect/card_locker/ImportURITest.java +++ b/app/src/test/java/protect/card_locker/ImportURITest.java @@ -11,6 +11,7 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.io.InvalidObjectException; +import java.math.BigDecimal; import java.util.Date; import static org.junit.Assert.assertEquals; @@ -38,7 +39,7 @@ public class ImportURITest { // Generate card Date date = new Date(); - db.insertLoyaltyCard("store", "note", date, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, Color.BLACK, 1); + db.insertLoyaltyCard("store", "note", date, new BigDecimal("0"), null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, Color.BLACK, 1); // Get card LoyaltyCard card = db.getLoyaltyCard(1); @@ -64,7 +65,7 @@ public class ImportURITest { public void ensureNoCrashOnMissingHeaderFields() throws InvalidObjectException { // Generate card - db.insertLoyaltyCard("store", "note", null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, null, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, null, 0); // Get card LoyaltyCard card = db.getLoyaltyCard(1); diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java index 3d8c25e2a..65d65e6fa 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java @@ -21,6 +21,7 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.math.BigDecimal; import java.text.DateFormat; import java.util.Date; @@ -102,7 +103,7 @@ public class LoyaltyCardCursorAdapterTest @Test public void TestCursorAdapterEmptyNote() { - db.insertLoyaltyCard("store", "", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("store", "", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); LoyaltyCard card = db.getLoyaltyCard(1); Cursor cursor = db.getLoyaltyCardCursor(); @@ -118,7 +119,7 @@ public class LoyaltyCardCursorAdapterTest @Test public void TestCursorAdapterWithNote() { - db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); LoyaltyCard card = db.getLoyaltyCard(1); Cursor cursor = db.getLoyaltyCardCursor(); @@ -138,7 +139,7 @@ public class LoyaltyCardCursorAdapterTest Date expiryDate = new Date(); String dateString = context.getString(R.string.expiryStateSentence, DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate)); - db.insertLoyaltyCard("store", "note", expiryDate, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", expiryDate, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); LoyaltyCard card = db.getLoyaltyCard(1); Cursor cursor = db.getLoyaltyCardCursor(); @@ -159,9 +160,9 @@ public class LoyaltyCardCursorAdapterTest @Test public void TestCursorAdapterStarring() { - db.insertLoyaltyCard("storeA", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); - db.insertLoyaltyCard("storeB", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); - db.insertLoyaltyCard("storeC", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); + db.insertLoyaltyCard("storeA", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("storeB", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); + db.insertLoyaltyCard("storeC", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); Cursor cursor = db.getLoyaltyCardCursor(); 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 d203f0393..6c0430c93 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -39,6 +39,7 @@ import com.google.android.material.textfield.TextInputLayout; import com.google.zxing.BarcodeFormat; import com.google.zxing.client.android.Intents; import java.io.IOException; +import java.math.BigDecimal; import java.text.DateFormat; import java.util.Date; import org.junit.Before; @@ -440,7 +441,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -458,7 +459,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -476,7 +477,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -499,7 +500,7 @@ public class LoyaltyCardViewActivityTest LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -537,7 +538,7 @@ public class LoyaltyCardViewActivityTest final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -570,7 +571,7 @@ public class LoyaltyCardViewActivityTest final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", new Date(), EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", new Date(), new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -594,7 +595,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -642,7 +643,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -662,7 +663,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, null, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, null, 0); activityController.start(); activityController.visible(); @@ -682,7 +683,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, null, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, null, 0); activityController.start(); activityController.visible(); @@ -701,7 +702,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, "", Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, "", Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -721,7 +722,7 @@ public class LoyaltyCardViewActivityTest final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); @@ -750,7 +751,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); final int STORE_FONT_SIZE = 50; final int CARD_FONT_SIZE = 40; @@ -790,7 +791,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(activity); settings.edit() @@ -827,7 +828,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity) activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK,0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK,0); activityController.start(); activityController.visible(); activityController.resume(); @@ -862,7 +863,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); diff --git a/app/src/test/java/protect/card_locker/MainActivityTest.java b/app/src/test/java/protect/card_locker/MainActivityTest.java index 5dcee2a93..34c35c06e 100644 --- a/app/src/test/java/protect/card_locker/MainActivityTest.java +++ b/app/src/test/java/protect/card_locker/MainActivityTest.java @@ -21,6 +21,7 @@ import org.robolectric.annotation.Config; import org.robolectric.android.controller.ActivityController; import org.robolectric.shadows.ShadowActivity; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -95,7 +96,7 @@ public class MainActivityTest assertEquals(0, list.getCount()); DBHelper db = new DBHelper(mainActivity); - db.insertLoyaltyCard("store", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); assertEquals(View.VISIBLE, helpText.getVisibility()); assertEquals(View.GONE, noMatchingCardsText.getVisibility()); @@ -131,10 +132,10 @@ public class MainActivityTest assertEquals(0, list.getCount()); DBHelper db = new DBHelper(mainActivity); - db.insertLoyaltyCard("storeB", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); - db.insertLoyaltyCard("storeA", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); - db.insertLoyaltyCard("storeD", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); - db.insertLoyaltyCard("storeC", "note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); + db.insertLoyaltyCard("storeB", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("storeA", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("storeD", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); + db.insertLoyaltyCard("storeC", "note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 1); assertEquals(View.VISIBLE, helpText.getVisibility()); assertEquals(View.GONE, noMatchingCardsText.getVisibility()); @@ -232,8 +233,8 @@ public class MainActivityTest TabLayout groupTabs = mainActivity.findViewById(R.id.groups); DBHelper db = new DBHelper(mainActivity); - db.insertLoyaltyCard("The First Store", "Initial note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); - db.insertLoyaltyCard("The Second Store", "Secondary note", null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("The First Store", "Initial note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("The Second Store", "Secondary note", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); db.insertGroup("Group one"); List groups = new ArrayList<>(); From d4f62435ae615f70bafaf8d4ac7fdde9e8e5f11e Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 20 Feb 2021 22:10:23 +0100 Subject: [PATCH 08/77] Add tests for balance --- .../card_locker/CsvDatabaseImporter.java | 2 +- .../java/protect/card_locker/DBHelper.java | 4 +- .../protect/card_locker/ImportURIHelper.java | 2 +- .../card_locker/LoyaltyCardCursorAdapter.java | 4 +- .../card_locker/LoyaltyCardEditActivity.java | 2 +- .../card_locker/LoyaltyCardViewActivity.java | 2 +- .../main/java/protect/card_locker/Utils.java | 4 +- app/src/main/res/values/strings.xml | 1 + .../protect/card_locker/DatabaseTest.java | 21 ++- .../protect/card_locker/ImportExportTest.java | 29 ++- .../protect/card_locker/ImportURITest.java | 12 +- .../LoyaltyCardCursorAdapterTest.java | 90 ++++++++- .../LoyaltyCardViewActivityTest.java | 177 +++++++++++++++--- 13 files changed, 300 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java index a4105002c..5ba74393e 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java @@ -309,7 +309,7 @@ public class CsvDatabaseImporter implements DatabaseImporter } catch (FormatException _e ) { // These fields did not exist in versions 1.8.1 and before // We catch this exception so we can still import old backups - balance = new BigDecimal("0.0"); + balance = new BigDecimal("0"); } Currency balanceType = null; diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index d25de0cb3..10a631eca 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -70,7 +70,7 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbIds.STORE + " TEXT not null," + LoyaltyCardDbIds.NOTE + " TEXT not null," + LoyaltyCardDbIds.EXPIRY + " INTEGER," + - LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0.0'," + + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'," + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT," + LoyaltyCardDbIds.HEADER_COLOR + " INTEGER," + LoyaltyCardDbIds.HEADER_TEXT_COLOR + " INTEGER," + @@ -139,7 +139,7 @@ public class DBHelper extends SQLiteOpenHelper if(oldVersion < 8 && newVersion >= 8) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE - + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0.0'"); + + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'"); db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT"); } diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index 776f44832..54add450f 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -47,7 +47,7 @@ public class ImportURIHelper { try { // These values are allowed to be null Date expiry = null; - BigDecimal balance = new BigDecimal("0.0"); + BigDecimal balance = new BigDecimal("0"); Currency balanceType = null; Integer headerColor = null; Integer headerTextColor = null; diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index 4f069c5ed..e20a03769 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -65,9 +65,9 @@ class LoyaltyCardCursorAdapter extends CursorAdapter noteField.setVisibility(View.GONE); } - if(!loyaltyCard.balance.equals(new BigDecimal(0))) { + if(!loyaltyCard.balance.equals(new BigDecimal("0"))) { balanceField.setVisibility(View.VISIBLE); - balanceField.setText(context.getString(R.string.balanceSentence, Utils.formatBalance(loyaltyCard.balance, loyaltyCard.balanceType))); + balanceField.setText(context.getString(R.string.balanceSentence, Utils.formatBalance(context, loyaltyCard.balance, loyaltyCard.balanceType))); } else { diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 2d5970b55..dd7e7606b 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -479,7 +479,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity setTitle(R.string.addCardTitle); expiryField.setTag(null); expiryField.setText(getString(R.string.never)); - balanceField.setTag(new BigDecimal("0.0")); + balanceField.setTag(new BigDecimal("0")); balanceCurrencyField.setTag(null); formatBalanceCurrencyField(null); hideBarcode(); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index c1e7ae719..db505dbd0 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -292,7 +292,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity if(!loyaltyCard.balance.equals(new BigDecimal(0))) { balanceView.setVisibility(View.VISIBLE); - balanceView.setText(getString(R.string.balanceSentence, Utils.formatBalance(loyaltyCard.balance, loyaltyCard.balanceType))); + balanceView.setText(getString(R.string.balanceSentence, Utils.formatBalance(this, loyaltyCard.balance, loyaltyCard.balanceType))); } else { diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 6013e4619..abbfb2edb 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -83,12 +83,12 @@ public class Utils { return expiryDate.before(date.getTime()); } - static public String formatBalance(BigDecimal value, Currency currency) { + static public String formatBalance(Context context, BigDecimal value, Currency currency) { NumberFormat numberFormat = NumberFormat.getInstance(); if (currency == null) { numberFormat.setMaximumFractionDigits(0); - return numberFormat.format(value); + return context.getString(R.string.balancePoints, numberFormat.format(value)); } NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc4c826b8..71260b998 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ Expires: %s Expired: %s Balance: %s + %s points Card Barcode diff --git a/app/src/test/java/protect/card_locker/DatabaseTest.java b/app/src/test/java/protect/card_locker/DatabaseTest.java index 7fa31444a..a1f6b11da 100644 --- a/app/src/test/java/protect/card_locker/DatabaseTest.java +++ b/app/src/test/java/protect/card_locker/DatabaseTest.java @@ -17,6 +17,7 @@ import org.robolectric.annotation.Config; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Currency; import java.util.List; import static org.junit.Assert.assertEquals; @@ -54,6 +55,8 @@ public class DatabaseTest assertEquals("store", loyaltyCard.store); assertEquals("note", loyaltyCard.note); assertEquals(null, loyaltyCard.expiry); + assertEquals(new BigDecimal("0"), loyaltyCard.balance); + assertEquals(null, loyaltyCard.balanceType); assertEquals("cardId", loyaltyCard.cardId); assertEquals(0, loyaltyCard.starStatus); assertEquals(BarcodeFormat.UPC_A.toString(), loyaltyCard.barcodeType); @@ -72,7 +75,7 @@ public class DatabaseTest assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); - result = db.updateLoyaltyCard(1, "store1", "note1", null, new BigDecimal("0"), null, "cardId1", BarcodeFormat.AZTEC.toString(), DEFAULT_HEADER_COLOR); + result = db.updateLoyaltyCard(1, "store1", "note1", null, new BigDecimal("10.00"), Currency.getInstance("EUR"), "cardId1", BarcodeFormat.AZTEC.toString(), DEFAULT_HEADER_COLOR); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -81,6 +84,8 @@ public class DatabaseTest assertEquals("store1", loyaltyCard.store); assertEquals("note1", loyaltyCard.note); assertEquals(null, loyaltyCard.expiry); + assertEquals(new BigDecimal("10.00"), loyaltyCard.balance); + assertEquals(Currency.getInstance("EUR"), loyaltyCard.balanceType); assertEquals("cardId1", loyaltyCard.cardId); assertEquals(0, loyaltyCard.starStatus); assertEquals(BarcodeFormat.AZTEC.toString(), loyaltyCard.barcodeType); @@ -103,6 +108,8 @@ public class DatabaseTest assertEquals("store", loyaltyCard.store); assertEquals("note", loyaltyCard.note); assertEquals(null, loyaltyCard.expiry); + assertEquals(new BigDecimal("0"), loyaltyCard.balance); + assertEquals(null, loyaltyCard.balanceType); assertEquals("cardId", loyaltyCard.cardId); assertEquals(1, loyaltyCard.starStatus); assertEquals(BarcodeFormat.UPC_A.toString(), loyaltyCard.barcodeType); @@ -132,6 +139,8 @@ public class DatabaseTest assertEquals("", loyaltyCard.store); assertEquals("", loyaltyCard.note); assertEquals(null, loyaltyCard.expiry); + assertEquals(new BigDecimal("0"), loyaltyCard.balance); + assertEquals(null, loyaltyCard.balanceType); assertEquals("", loyaltyCard.cardId); assertEquals("", loyaltyCard.barcodeType); } @@ -165,6 +174,8 @@ public class DatabaseTest assertEquals("store"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE))); assertEquals("note"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE))); assertEquals(0, cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY))); + assertEquals("0", cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); + assertEquals(null, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE))); assertEquals("cardId"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID))); assertEquals(BarcodeFormat.UPC_A.toString(), cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE))); assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS))); @@ -212,6 +223,8 @@ public class DatabaseTest assertEquals("store"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE))); assertEquals("note"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE))); assertEquals(0, cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY))); + assertEquals("0", cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); + assertEquals(null, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE))); assertEquals("cardId"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID))); assertEquals(BarcodeFormat.UPC_A.toString(), cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE))); assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS))); @@ -223,7 +236,9 @@ public class DatabaseTest { assertEquals("store"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE))); assertEquals("note"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE))); - assertEquals("cardId"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID))); + assertEquals(null, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY))); + assertEquals("0", cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE))); + assertEquals(null, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE))); assertEquals("cardId"+index, cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID))); assertEquals(BarcodeFormat.UPC_A.toString(), cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE))); assertEquals(0, cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS))); @@ -466,6 +481,8 @@ public class DatabaseTest LoyaltyCard card = db.getLoyaltyCard(newCardId); assertEquals("store", card.store); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("cardId", card.cardId); assertEquals(BarcodeFormat.UPC_A.toString(), card.barcodeType); assertEquals("", card.note); diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 831644377..2baf9ee73 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -105,7 +105,8 @@ public class ImportExportTest assertEquals(cardsToAdd, db.getLoyaltyCardCount()); } - private void addLoyaltyCardsWithExpiryNeverPastTodayFuture() + @Test + public void addLoyaltyCardsWithExpiryNeverPastTodayFuture() { long id = db.insertLoyaltyCard("No Expiry", "", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); boolean result = (id != -1); @@ -161,6 +162,8 @@ public class ImportExportTest assertEquals(expectedStore, card.store); assertEquals(expectedNote, card.note); + assertEquals(new BigDecimal(String.valueOf(index)), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(index), card.headerColor); @@ -191,6 +194,8 @@ public class ImportExportTest assertEquals(expectedStore, card.store); assertEquals(expectedNote, card.note); + assertEquals(new BigDecimal(String.valueOf(index)), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(index), card.headerColor); @@ -209,6 +214,8 @@ public class ImportExportTest assertEquals(expectedStore, card.store); assertEquals(expectedNote, card.note); + assertEquals(new BigDecimal(String.valueOf(index)), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(index), card.headerColor); @@ -577,6 +584,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("type", card.barcodeType); assertEquals(0, card.starStatus); @@ -613,6 +622,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("type", card.barcodeType); assertEquals(0, card.starStatus); @@ -675,6 +686,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("", card.barcodeType); assertEquals(0, card.starStatus); @@ -711,6 +724,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("type", card.barcodeType); assertEquals(1, card.starStatus); @@ -747,6 +762,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("type", card.barcodeType); assertEquals(0, card.starStatus); @@ -803,6 +820,8 @@ public class ImportExportTest assertEquals("store", card.store); assertEquals("note", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals("12345", card.cardId); assertEquals("type", card.barcodeType); assertEquals(0, card.starStatus); @@ -820,6 +839,8 @@ public class ImportExportTest assertEquals("Never", card.store); assertEquals("", card.note); assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(0), card.headerColor); @@ -830,6 +851,8 @@ public class ImportExportTest assertEquals("Past", card.store); assertEquals("", card.note); assertTrue(card.expiry.before(new Date())); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(0), card.headerColor); @@ -841,6 +864,8 @@ public class ImportExportTest assertEquals("", card.note); assertTrue(card.expiry.before(new Date(new Date().getTime()+86400))); assertTrue(card.expiry.after(new Date(new Date().getTime()-86400))); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(0), card.headerColor); @@ -851,6 +876,8 @@ public class ImportExportTest assertEquals("Future", card.store); assertEquals("", card.note); assertTrue(card.expiry.after(new Date(new Date().getTime()+86400))); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); assertEquals(BARCODE_DATA, card.cardId); assertEquals(BARCODE_TYPE, card.barcodeType); assertEquals(Integer.valueOf(0), card.headerColor); diff --git a/app/src/test/java/protect/card_locker/ImportURITest.java b/app/src/test/java/protect/card_locker/ImportURITest.java index 56b8153e8..2b29c47cb 100644 --- a/app/src/test/java/protect/card_locker/ImportURITest.java +++ b/app/src/test/java/protect/card_locker/ImportURITest.java @@ -12,6 +12,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.io.InvalidObjectException; import java.math.BigDecimal; +import java.util.Currency; import java.util.Date; import static org.junit.Assert.assertEquals; @@ -39,7 +40,7 @@ public class ImportURITest { // Generate card Date date = new Date(); - db.insertLoyaltyCard("store", "note", date, new BigDecimal("0"), null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, Color.BLACK, 1); + db.insertLoyaltyCard("store", "note", date, new BigDecimal("100"), null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, Color.BLACK, 1); // Get card LoyaltyCard card = db.getLoyaltyCard(1); @@ -56,6 +57,8 @@ public class ImportURITest { assertEquals(card.headerColor, parsedCard.headerColor); assertEquals(card.note, parsedCard.note); assertEquals(card.expiry, parsedCard.expiry); + assertEquals(card.balance, parsedCard.balance); + assertEquals(card.balanceType, parsedCard.balanceType); assertEquals(card.store, parsedCard.store); // No export of starStatus for single cards foreseen therefore 0 will be imported assertEquals(0, parsedCard.starStatus); @@ -65,7 +68,7 @@ public class ImportURITest { public void ensureNoCrashOnMissingHeaderFields() throws InvalidObjectException { // Generate card - db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, null, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("EUR"), BarcodeFormat.UPC_A.toString(), LoyaltyCardDbIds.BARCODE_TYPE, null, 0); // Get card LoyaltyCard card = db.getLoyaltyCard(1); @@ -81,6 +84,8 @@ public class ImportURITest { assertEquals(card.cardId, parsedCard.cardId); assertEquals(card.note, parsedCard.note); assertEquals(card.expiry, parsedCard.expiry); + assertEquals(card.balance, parsedCard.balance); + assertEquals(card.balanceType, parsedCard.balanceType); assertEquals(card.store, parsedCard.store); assertNull(parsedCard.headerColor); assertNull(parsedCard.headerTextColor); @@ -126,5 +131,8 @@ public class ImportURITest { assertEquals("store", parsedCard.store); assertEquals(Integer.valueOf(-416706), parsedCard.headerColor); assertEquals(0, parsedCard.starStatus); + assertEquals(null, parsedCard.expiry); + assertEquals(new BigDecimal("0"), parsedCard.balance); + assertEquals(null, parsedCard.balanceType); } } diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java index 65d65e6fa..6132d1308 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java @@ -23,6 +23,7 @@ import org.robolectric.annotation.Config; import java.math.BigDecimal; import java.text.DateFormat; +import java.util.Currency; import java.util.Date; import static org.junit.Assert.assertEquals; @@ -61,11 +62,12 @@ public class LoyaltyCardCursorAdapterTest return view; } - private void checkView(final View view, final String store, final String note, final String expiry, boolean checkFontSizes) + private void checkView(final View view, final String store, final String note, final String expiry, final String balance, boolean checkFontSizes) { final TextView storeField = view.findViewById(R.id.store); final TextView noteField = view.findViewById(R.id.note); final TextView expiryField = view.findViewById(R.id.expiry); + final TextView balanceField = view.findViewById(R.id.balance); if(checkFontSizes) { @@ -78,7 +80,7 @@ public class LoyaltyCardCursorAdapterTest } assertEquals(store, storeField.getText().toString()); - if(note.isEmpty() == false) + if(!note.isEmpty()) { assertEquals(View.VISIBLE, noteField.getVisibility()); assertEquals(note, noteField.getText().toString()); @@ -88,7 +90,7 @@ public class LoyaltyCardCursorAdapterTest assertEquals(View.GONE, noteField.getVisibility()); } - if(expiry.isEmpty() == false) + if(!expiry.isEmpty()) { assertEquals(View.VISIBLE, expiryField.getVisibility()); assertEquals(expiry, expiryField.getText().toString()); @@ -97,6 +99,16 @@ public class LoyaltyCardCursorAdapterTest { assertEquals(View.GONE, expiryField.getVisibility()); } + + if(!balance.isEmpty()) + { + assertEquals(View.VISIBLE, balanceField.getVisibility()); + assertEquals(balance, balanceField.getText().toString()); + } + else + { + assertEquals(View.GONE, balanceField.getVisibility()); + } } @@ -111,7 +123,7 @@ public class LoyaltyCardCursorAdapterTest View view = createView(cursor); - checkView(view, card.store, card.note, "", false); + checkView(view, card.store, card.note, "", "",false); cursor.close(); } @@ -127,7 +139,7 @@ public class LoyaltyCardCursorAdapterTest View view = createView(cursor); - checkView(view, card.store, card.note, "", false); + checkView(view, card.store, card.note, "", "",false); cursor.close(); } @@ -148,11 +160,11 @@ public class LoyaltyCardCursorAdapterTest setFontSizes(1, 2); View view = createView(cursor); - checkView(view, card.store, card.note, dateString, true); + checkView(view, card.store, card.note, dateString, "", true); setFontSizes(30, 31); view = createView(cursor); - checkView(view, card.store, card.note, dateString, true); + checkView(view, card.store, card.note, dateString, "",true); cursor.close(); } @@ -182,4 +194,68 @@ public class LoyaltyCardCursorAdapterTest cursor.close(); } + + @Test + public void TestCursorAdapter0Points() + { + db.insertLoyaltyCard("store", "", null, new BigDecimal("0"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + LoyaltyCard card = db.getLoyaltyCard(1); + + Cursor cursor = db.getLoyaltyCardCursor(); + cursor.moveToFirst(); + + View view = createView(cursor); + + checkView(view, card.store, card.note, "", "",false); + + cursor.close(); + } + + @Test + public void TestCursorAdapter0EUR() + { + db.insertLoyaltyCard("store", "", null, new BigDecimal("0"), Currency.getInstance("EUR"), "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + LoyaltyCard card = db.getLoyaltyCard(1); + + Cursor cursor = db.getLoyaltyCardCursor(); + cursor.moveToFirst(); + + View view = createView(cursor); + + checkView(view, card.store, card.note, "", "",false); + + cursor.close(); + } + + @Test + public void TestCursorAdapter100Points() + { + db.insertLoyaltyCard("store", "note", null, new BigDecimal("100"), null, "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + LoyaltyCard card = db.getLoyaltyCard(1); + + Cursor cursor = db.getLoyaltyCardCursor(); + cursor.moveToFirst(); + + View view = createView(cursor); + + checkView(view, card.store, card.note, "", "Balance: 100 points",false); + + cursor.close(); + } + + @Test + public void TestCursorAdapter10Eur() + { + db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("EUR"), "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + LoyaltyCard card = db.getLoyaltyCard(1); + + Cursor cursor = db.getLoyaltyCardCursor(); + cursor.moveToFirst(); + + View view = createView(cursor); + + checkView(view, card.store, card.note, "", "Balance: €10.00",false); + + cursor.close(); + } } diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index 6c0430c93..6a2b7f4b5 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -41,6 +41,7 @@ import com.google.zxing.client.android.Intents; import java.io.IOException; import java.math.BigDecimal; import java.text.DateFormat; +import java.util.Currency; import java.util.Date; import org.junit.Before; import org.junit.Test; @@ -109,6 +110,8 @@ public class LoyaltyCardViewActivityTest private void saveLoyaltyCardWithArguments(final Activity activity, final String store, final String note, final Date expiry, + final BigDecimal balance, + final Currency balanceType, final String cardId, final String barcodeType, boolean creatingNewCard) @@ -126,12 +129,20 @@ public class LoyaltyCardViewActivityTest final EditText storeField = activity.findViewById(R.id.storeNameEdit); final EditText noteField = activity.findViewById(R.id.noteEdit); final TextInputLayout expiryView = activity.findViewById(R.id.expiryView); + final EditText balanceView = activity.findViewById(R.id.balanceField); + final EditText balanceCurrencyField = activity.findViewById(R.id.balanceCurrencyField); final TextView cardIdField = activity.findViewById(R.id.cardIdView); final TextView barcodeTypeField = activity.findViewById(R.id.barcodeTypeField); storeField.setText(store); noteField.setText(note); expiryView.setTag(expiry); + if (balance != null) { + balanceView.setText(balance.toPlainString()); + } + if (balanceType != null) { + balanceCurrencyField.setText(balanceType.getSymbol()); + } cardIdField.setText(cardId); barcodeTypeField.setText(barcodeType); @@ -144,6 +155,13 @@ public class LoyaltyCardViewActivityTest LoyaltyCard card = db.getLoyaltyCard(1); assertEquals(store, card.store); assertEquals(note, card.note); + assertEquals(expiry, card.expiry); + if (balance != null) { + assertEquals(balance, card.balance); + } else { + assertEquals(new BigDecimal("0"), card.balance); + } + assertEquals(balanceType, card.balanceType); assertEquals(cardId, card.cardId); // The special "No barcode" string shouldn't actually be written to the loyalty card @@ -254,7 +272,9 @@ public class LoyaltyCardViewActivityTest } private void checkAllFields(final Activity activity, ViewMode mode, - final String store, final String note, final String expiryString, final String cardId, final String barcodeType) + final String store, final String note, final String expiryString, + final String balanceString, final String balanceTypeString, + final String cardId, final String barcodeType) { if(mode == ViewMode.VIEW_CARD) { @@ -267,6 +287,8 @@ public class LoyaltyCardViewActivityTest checkFieldProperties(activity, R.id.storeNameEdit, editVisibility, store); checkFieldProperties(activity, R.id.noteEdit, editVisibility, note); checkFieldProperties(activity, R.id.expiryView, editVisibility, expiryString); + checkFieldProperties(activity, R.id.balanceField, editVisibility, balanceString); + checkFieldProperties(activity, R.id.balanceCurrencyField, editVisibility, balanceTypeString); checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId); checkFieldProperties(activity, R.id.barcodeTypeField, View.VISIBLE, barcodeType); checkFieldProperties(activity, R.id.barcode, View.VISIBLE, null); @@ -284,7 +306,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never) , "", ""); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never) , "0", context.getString(R.string.points), "", ""); } @Test @@ -302,7 +324,6 @@ public class LoyaltyCardViewActivityTest final EditText storeField = activity.findViewById(R.id.storeNameEdit); final EditText noteField = activity.findViewById(R.id.noteEdit); - final TextView cardIdField = activity.findViewById(R.id.cardIdView); activity.findViewById(R.id.fabSave).performClick(); assertEquals(0, db.getLoyaltyCardCount()); @@ -346,15 +367,17 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "", ""); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", ""); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); + + shadowOf(getMainLooper()).idle(); // Save and check the loyalty card - saveLoyaltyCardWithArguments(activity, "store", "note", null, BARCODE_DATA, BARCODE_TYPE, true); + saveLoyaltyCardWithArguments(activity, "store", "note", null, null, null, BARCODE_DATA, BARCODE_TYPE, true); } @Test @@ -368,12 +391,12 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "", ""); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", ""); // Complete barcode capture in failure captureBarcodeWithResult(activity, false); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "", ""); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", ""); } @Test @@ -387,12 +410,12 @@ public class LoyaltyCardViewActivityTest LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "", ""); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", ""); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); // Cancel the loyalty card creation assertEquals(false, activity.isFinishing()); @@ -439,6 +462,7 @@ public class LoyaltyCardViewActivityTest { ActivityController activityController = createActivityWithLoyaltyCard(true); Activity activity = (Activity)activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); @@ -447,7 +471,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); db.close(); } @@ -457,6 +481,7 @@ public class LoyaltyCardViewActivityTest { ActivityController activityController = createActivityWithLoyaltyCard(false); Activity activity = (Activity)activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, Color.BLACK, 0); @@ -465,7 +490,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.VIEW_CARD, "store", "note", null, BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.VIEW_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); db.close(); } @@ -475,6 +500,7 @@ public class LoyaltyCardViewActivityTest { ActivityController activityController = createActivityWithLoyaltyCard(true); Activity activity = (Activity)activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); @@ -483,12 +509,12 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); db.close(); } @@ -498,6 +524,7 @@ public class LoyaltyCardViewActivityTest { ActivityController activityController = createActivityWithLoyaltyCard(true); LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); @@ -506,12 +533,12 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); // Complete barcode capture successfully captureBarcodeWithResult(activity, true); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); // Cancel the loyalty card creation assertEquals(false, activity.isFinishing()); @@ -544,7 +571,7 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); // Set date to today MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField); @@ -558,7 +585,7 @@ public class LoyaltyCardViewActivityTest shadowOf(getMainLooper()).idle(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); db.close(); } @@ -577,13 +604,105 @@ public class LoyaltyCardViewActivityTest activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); // Set date to never MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField); expiryField.setText(expiryField.getAdapter().getItem(0).toString(), false); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + + db.close(); + } + + @Test + public void startWithLoyaltyCardNoBalanceSetBalance() throws IOException + { + ActivityController activityController = createActivityWithLoyaltyCard(true); + Activity activity = (Activity)activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); + DBHelper db = new DBHelper(activity); + + db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + + activityController.start(); + activityController.visible(); + activityController.resume(); + + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + + // Set balance to 10 points + EditText balanceField = activity.findViewById(R.id.balanceField); + balanceField.setText("10"); + + shadowOf(getMainLooper()).idle(); + + // Change points to EUR + MaterialAutoCompleteTextView balanceTypeField = activity.findViewById(R.id.balanceCurrencyField); + balanceTypeField.setText("€", false); + + shadowOf(getMainLooper()).idle(); + + // Ensure the balance is reformatted for EUR when focus is cleared + shadowOf(getMainLooper()).idle(); + balanceField.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + assertEquals("10.00", balanceField.getText().toString()); + + shadowOf(getMainLooper()).idle(); + + DatePickerDialog datePickerDialog = (DatePickerDialog) (ShadowDialog.getLatestDialog()); + assertNotNull(datePickerDialog); + datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE).performClick(); + + shadowOf(getMainLooper()).idle(); + + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "€", EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + + db.close(); + } + }); + balanceField.clearFocus(); + } + + @Test + public void startWithLoyaltyCardBalanceSetNoBalance() throws IOException + { + ActivityController activityController = createActivityWithLoyaltyCard(true); + Activity activity = (Activity)activityController.get(); + final Context context = ApplicationProvider.getApplicationContext(); + DBHelper db = new DBHelper(activity); + + db.insertLoyaltyCard("store", "note", new Date(), new BigDecimal("10.00"), Currency.getInstance("EUR"), EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + + activityController.start(); + activityController.visible(); + activityController.resume(); + + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "€", EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + + shadowOf(getMainLooper()).idle(); + + // Change EUR to WON + MaterialAutoCompleteTextView balanceTypeField = activity.findViewById(R.id.balanceCurrencyField); + balanceTypeField.setText("₩", false); + + shadowOf(getMainLooper()).idle(); + + // Ensure the balance is reformatted for WON when focus is cleared + EditText balanceField = activity.findViewById(R.id.balanceField); + balanceField.clearFocus(); + assertEquals("10", balanceField.getText().toString()); + + shadowOf(getMainLooper()).idle(); + + // Set the balance to 0 + balanceField.setText("0"); + + shadowOf(getMainLooper()).idle(); + + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", "₩", EAN_BARCODE_DATA, EAN_BARCODE_TYPE); db.close(); } @@ -690,7 +809,7 @@ public class LoyaltyCardViewActivityTest activityController.resume(); // Save and check the loyalty card - saveLoyaltyCardWithArguments(activity, "store", "note", null, BARCODE_DATA, BARCODE_TYPE, false); + saveLoyaltyCardWithArguments(activity, "store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, false); db.close(); } @@ -709,7 +828,7 @@ public class LoyaltyCardViewActivityTest activityController.resume(); // Save and check the loyalty card - saveLoyaltyCardWithArguments(activity, "store", "note", null, BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode), false); + saveLoyaltyCardWithArguments(activity, "store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode), false); db.close(); } @@ -729,17 +848,17 @@ public class LoyaltyCardViewActivityTest activityController.resume(); // First check if the card is as expected - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), BARCODE_DATA, BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, BARCODE_TYPE); // Complete empty barcode selection successfully selectBarcodeWithResult(activity, BARCODE_DATA, "", true); // Check if the barcode type is NO_BARCODE as expected - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode)); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode)); assertEquals(View.GONE, activity.findViewById(R.id.barcodeLayout).getVisibility()); // Check if the special NO_BARCODE string doesn't get saved - saveLoyaltyCardWithArguments(activity, "store", "note", null, BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode), false); + saveLoyaltyCardWithArguments(activity, "store", "note", null, null, null, BARCODE_DATA, activity.getApplicationContext().getString(R.string.noBarcode), false); db.close(); } @@ -961,7 +1080,7 @@ public class LoyaltyCardViewActivityTest { Date date = new Date(); - Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); + Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&balance=10&balanceType=EUR&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); Intent intent = new Intent(); intent.setData(importUri); @@ -975,7 +1094,9 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "123456", "AZTEC"); + shadowOf(getMainLooper()).idle(); + + checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "€", "123456", "AZTEC"); assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor()); } @@ -996,7 +1117,7 @@ public class LoyaltyCardViewActivityTest Activity activity = (Activity)activityController.get(); final Context context = ApplicationProvider.getApplicationContext(); - checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "123456", "AZTEC"); + checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", "AZTEC"); assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor()); } } From 9a37d917f7374abc72950e3a8bf0c278c52fb016 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 20 Feb 2021 22:59:51 +0100 Subject: [PATCH 09/77] Fix test URL --- .../java/protect/card_locker/LoyaltyCardViewActivityTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index 6a2b7f4b5..6eb668698 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -1080,7 +1080,7 @@ public class LoyaltyCardViewActivityTest { Date date = new Date(); - Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&balance=10&balanceType=EUR&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); + Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&balance=10&balancetype=EUR&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); Intent intent = new Intent(); intent.setData(importUri); From 0af1935d205287b7e0dac3c3b2146ec59f46c39f Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 21 Feb 2021 18:00:06 +0100 Subject: [PATCH 10/77] Switch tests to USD --- .../protect/card_locker/LoyaltyCardCursorAdapterTest.java | 6 +++--- .../protect/card_locker/LoyaltyCardViewActivityTest.java | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java index 6132d1308..8055e8026 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardCursorAdapterTest.java @@ -244,9 +244,9 @@ public class LoyaltyCardCursorAdapterTest } @Test - public void TestCursorAdapter10Eur() + public void TestCursorAdapter10USD() { - db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("EUR"), "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("USD"), "cardId", BarcodeFormat.UPC_A.toString(), Color.BLACK, 0); LoyaltyCard card = db.getLoyaltyCard(1); Cursor cursor = db.getLoyaltyCardCursor(); @@ -254,7 +254,7 @@ public class LoyaltyCardCursorAdapterTest View view = createView(cursor); - checkView(view, card.store, card.note, "", "Balance: €10.00",false); + checkView(view, card.store, card.note, "", "Balance: $10.00",false); cursor.close(); } diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index 6eb668698..3fa5926ae 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -674,13 +674,13 @@ public class LoyaltyCardViewActivityTest final Context context = ApplicationProvider.getApplicationContext(); DBHelper db = new DBHelper(activity); - db.insertLoyaltyCard("store", "note", new Date(), new BigDecimal("10.00"), Currency.getInstance("EUR"), EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); + db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("USD"), EAN_BARCODE_DATA, EAN_BARCODE_TYPE, Color.BLACK, 0); activityController.start(); activityController.visible(); activityController.resume(); - checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "€", EAN_BARCODE_DATA, EAN_BARCODE_TYPE); + checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "10.00", "$", EAN_BARCODE_DATA, EAN_BARCODE_TYPE); shadowOf(getMainLooper()).idle(); @@ -1080,7 +1080,7 @@ public class LoyaltyCardViewActivityTest { Date date = new Date(); - Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&balance=10&balancetype=EUR&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); + Uri importUri = Uri.parse("https://thelastproject.github.io/Catima/share?store=Example%20Store¬e=&expiry=" + date.getTime() + "&balance=10&balancetype=USD&cardid=123456&barcodetype=AZTEC&headercolor=-416706&headertextcolor=-1"); Intent intent = new Intent(); intent.setData(importUri); @@ -1096,7 +1096,7 @@ public class LoyaltyCardViewActivityTest shadowOf(getMainLooper()).idle(); - checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "€", "123456", "AZTEC"); + checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", "AZTEC"); assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor()); } From d54f574558e23c6260b345c46bf664a30674ec2e Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 21 Feb 2021 18:10:56 +0100 Subject: [PATCH 11/77] Update screenshots --- .../images/phoneScreenshots/screenshot-02.png | Bin 71297 -> 65125 bytes .../images/phoneScreenshots/screenshot-06.png | Bin 68033 -> 60332 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-02.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-02.png index 9be5728578c3043788eb3e110281f7adee3df073..505c8dd9f30572c2f9c00deaa85fbb2783c7b35b 100644 GIT binary patch literal 65125 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 literal 71297 zcmce;by$?^+BfW4E(-|-=~5{fq+0|O1f**aX^|G`7Cz5ULdEn1w2MJl|_3PKiW|XJF*WVn}WF#)-chRg|`i<(gwUAUE2qyFsVWQH4=ka=*jtLs zH!vuMZOwRJvy_)_7qA|$w4L#di@Q?mzBK8 |hI&?}Bf31)^qz|q*ZRcvtF_SKB2JbB5)pDFn@9_TP-|5RIjvDt2&fc^y?lqOyo$VCBX)y#o*o~7!Z1gc(T>kNA z#^@O&niI>jHJ}-Wuh7?$R8`fuRnxbmGgUOAqs2;)YFQS`qHNu1JrN!`#Oha&7pF{d zMp(DsYbQ9)m1;SZi?n4U8M3IiD&L3_6*+z4kGZ>9kUP#}A~(F8g(`b=*@ug*~>X>jfv54@wL8Ev?9Fj~55C z)V)czDj0hBcoV#;3}gB`tCtd5#$@#Eq;5_Qs3Jo1Ra_T2vAe%8XL+mLXD1B`y+`p# zOt6iD`{|C}l`BBhW^?AYzl4{3_(Q~mC{0VPJEF>wSkI(h?FMQ5#Gv`amd-Hd57%YI z5>++zH1u@ijF|`A4}&TXM?17;96S9OTRTu2*b{iWpp96s9du(-bX&7d&hvvf^)amp zX03oF#yn#AA+OWSnPpD*_<}R8DqLDsBIL|x`^n7 zxMpE>?LMSRLmtmVJATK`azdQfg?f9|3vxXc5;3xwCAn&k)HCrPEya=2!*$iwh#!$9!PG{a(b@SN`jth-8>3A# z->jN~D)w%Cqww1Qsi0d7vx!nZni{58BPC@QyA_fD7X^4Hm@TX)Bk$2y`A9zBHl3*x z{V2nrFM|2>g84f+J&|3~5++lJ8}xDZa&!K3$ea{N8i`Mvh&Z~ZM$^FR@m0$gna2I* zwmTwQ&zN5VU7lC_Nb9ip3@L_Q%`uuh7_Gxt0(IFlaL=n;lBCl~;>j`V=A3|TZ_jKO z-?cyJUYj~brWD0Y7@@%YEZ0u3Wk|ImQRpIazt=2eSym2` z6snr?<1-%tCnDc~{_MbuCC>I}`iF1i3y*!(j_UgklNu^_p~Tuq6Z!o?qTn%?n1Kdg z&n)H)<&fy<6yC0nNS6oehxiY`DfDmzO`?4$_IqBf5Ylj<_E*1ly?{RZ(jFsihq_H0#_ZVRJ zYxeGAiACm_2}W*1aJ=q7Y}zGu_M^nHylA{6$rDuFZa0=j^heRV%PC7Fhvwsr>?>1s zh$Xjl%MX40^r4K!?KRa~o8RT4k2`ofwa1Lup)tAPN~Dm3gGM&ZF3+7tS=8iMu1af3 z&#QSlH~DM$A6;#BvNZ@eH==l1b{m&4i#CV(T6@PsX5G%gauONh$HtI%J0r>gu6xND zL6uZuMbu(gBiWP&)-RH;damiW30Mb-XeHWs=FChESe4hW#1P4bYAv-r^?sD9CNMwG zhw?3JhGwR!vBon%>(ljds?ia{Rq+Ps%c%MG6DOyJp-A}l({(LFxR3xMSU86I&8?4B@uQ!8AHp0M7%@lf zPJ4fOI}w^l&zs|zXIr%h+|l63UIDhMHTGM2d}yiV9Hvq`lP?FS555L&7IQkRe1cFO zE54ViFNco`WebOfkYujuZ{|nWfmYnQ5nEvva|6K{RQbDCKSGl^hG)HInKcrAWH_G| zS%*1@(n)2}ng#wWg@w+!m8quy2}I*jn9l6>J_~+oG^!~SymwEQhuZVN0F&m{o#31k z4$L-UVgXBW1JTcxwkIl+b|op)BIU=yT$7ZNX&~K5m-_;O^J+0kV^}K`D2O3eJcpRc{6eyHUHY9N2g;9H?jsnkwx7B(bh$M-Oy!v|)MJ#4th| zf9Yb{A+`XE?p3rN4MI2gi9b5qkI0+#(eJ3gOUCNIiONsQAB{M2|6RSy*OLd2% z=`}T+bVI6Yy|!BN6e#IX(`6}glihB=pqKtdim^yXgFSU6S-u6}#ex#-*ZQ-13I^#P zh0kUg7%oY5vz$7b58g)n&HTeh!1l-PLA4#!Qx5SF^WCZzDSmo<5@WyaG$f@==@9Zh>X@-|@0CJk zhs$bK(@^HKd>0FkU^(hM+dy30c_D%=9U3iBdqGImAJcGWgA&pz%L6sKM^3g1sCbO+>byv->qPNAsrB1VuVpwcs!+n$@_GvB#B8VM46u((3A^D3In+%d zA;%w;UAKP_Y8e9PW9-d&)*$}#R9oRDLIj4~RmS$WIazkAWU;bFV}6qOdz?{EwfKPZ zjR;Y71e`zi#%*FRx<;B+Je%WLo|Ky$!=>h`b0J4RMK*5%-oE<_q7`SH~5{vBg03yb6sm(Uz79{}Vw7C-0f z^-(|?s-5K9wVjYE7A9%e5xLX^$djcIW%I&>G8L?r5$Cl<($RM|D7wOz*!^S;h%MI$via3R|Hs9r_HC!v8 z6nkvXveG2->NKz1m&jnAtu!r-wzPibtVuR>9Up)+kDcYJh$7eCR5Q#jr-&GoS)rvw zMOGf1WSxpYsp9M~>RN`6oUDCzPblW>LmT-fqed2bX^N0zE zHsc1Wx1%eP3vE~2w`zAbY{XH5a1#48ei@eD(t4|%;4sGM4dgZqu36-46)2N$LM@n% za%G5ThW2W(N-zaVd-o72Y4ICBZy-;>8q>?MdU(ovVUo^H7(_{~_DV1vnnN$A6d)h< zg!j?8KWx1`H-|>oosIpOc(y(OKaOnC^o6aFiG~St*8Eas8&97Sk_{cPOoU@mz|WB3 z>!37WAdlm>Qeh^KWHM8~0kb0uzREzPl*PeeU12$1D9_>FVXSU#J#X=@!RutKrJ=99 zGwL2xOz`b8r|7_ZD+`GtB_>`*S@`65NvE))P(Img7a-sfuvZsuFRgjMjCeBFMak^` zA-MhFh=oCz(k;kJ9xwv-bJv1x^j6d9) zy5qEtfl3^-=#tm!hE4G+z0bpi-!DM3w!ouXnEH8ny{&`#sOh zr4v~9=FlS^Ir36uzmD)6D~H>*vC2mDZZ(TOUmCwPuH{f2s(QzqYzN9);%5uvXKD>Rgj#otFR>`qtg&4;X-}dts4$u?5vb-6*FwbUX^V-pg~3eO5zb^3e1B8#xcuw& z^@6gMuMm03v<(QbON@Epj<-58^UGu{!ye8yo6mlN6u~jLjr+f%M*sbFQX(rlIxy}%5f+TR0Rm$&wcRFV@XE*ja_EA)ws}f zYFKCu3Wx4o<;SVNrvWT=0*rTrmDclMzz#0VTp7Q{ZKqwMbaBS&BoP5_VE%W!V`MZZr8d1oqnMcTf~?4w|#BvD;c5U>DZQ zLyFbdjS~nC`~GWb;a>^~e-#`4W#wN=3$kjNxXQ(UTDhoYTwHTe-59Y>;@Q-!yANn= z4f@v~{@<590Byr~|NCmmzt>Ox%OU^$NdIZ&Kb3GU81cU=;ruU%x{$@+MDeXu{dRfq zU~7xy39g2Oz|Rh6kkQ&3)DipRTyCvjkEXqVg#C^^|D@iWIn--kRU z3zJfTuXP7=B2_FzSP~q%+J^MmV)%2MyAv1=4uUo3o|u@7WNXA;B}#B=XBSJpc!sZB zbbo11h=j9Nsg=&0o)*$MPsQRU3ji7sGYv#9G-zeNpwYbx?Mvd@e-dSb+Ll^oX1Gzy zF=`^x+~e`uV(bSwH@(4}Axv*C1N&E`SMyJQl5Sy{% zH*niTJ0A>Ko*d*%R`T|EX^p4&so*Mic%+D4VSZ-UpXDZXJJv%1M={pyCNqep8Z%nIgb#Adqiv$AhSmd+aiwfLr33=qT%)CP6sax&ta zjIbAPhi@&Vcc-P1c%7}8cA6~)3zjYB))ZDCYTe1Z6X{WJq8qLe`oR0gnu2b~%i4L8 zp_Wz4>`j7l(1%toPD-dv_r*?t0?2E_O={gt8viDV4@1pT+*L?IofvxO>MuhfAxsc2 z;rbZ)UN2^MckxqR3c=nu&+BItw^-^Z((T(Lt`Ld#$NVs_W`EFVL~dtus1X%A7P_}T zJMr7Ye$ze~r>lhH1}`Go8lM|lNp_^#hnsfl*fdsJ*efZySXkA0*_C5=BSEgDCk=F3 zm394<=D`N_Qeo>)e}aO|I3A{hccx)VRm;gm8aZknF*BgLX#yZ{(fNi0!Q&aq)*&ath!H+?ebfu3h%Y*NZ6+$pCT)ivZ|_DJ!u7@&WlS0t|uG7uI4TQ4d@8?;J>pZuj<u_!AYtcHIx zn$siDo3afXVPr;+xhCV3EmLbT&RKgr+AnW(u)2bav*3y#&m{TQ7f!=50XFEWoct2# zA}o3cJO^qF@^r(=E95^b?VbO+Ca|E*$*lMHqGStKP`4E)CSHQeYpnwPoJkj>PLZwLjiG`c zvo6X=n?RM;;d9r}16iji|3pP<##r@QisYK%tf)v$Ys2qj4)(LU+guK&qvtk6MuA$P z_vcWyQD2&ZO#yRlsGF5^a_W3kGeQQ+HPt~l9FUh63j1;8(C&~5rA2A@Gx}7|Zp(>! zkK&|hGe#hf-)O)8uV^cFYkEo$NyB z>=N4qk7onJ>bQIQE|ZHMz6^zJ_`~Q_S0l|*FI-0sexPZ$b)u*kkT_8I(%tvp%B~|6 zCQWd$GOzy7xjRnSj!xI^X!~7205dQd^6HOc<44ZK~^T^)oL;rd;{&hnka?<=7SQT8PT# z+l+jP9gAt_Rih6o8+)%k4Qweu1gixqZ0{?r=*7L@uC74~IhukA_Q&#^je462$9oxi zP8!L_TkM$)IR~Yg) zb|+_l^VlRaut}Ya^W|p`%mx`Gy5`&iYJb>RS|76xJh{8gr(z(!mmWLI;kd9?Aei6* z*V4MsgosM`6tZcLQ>Qe$axdDkN6OB|;@44777i1bwV~0JLk5ZWl0s+BRU%nqSjn1N z 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 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-06.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-06.png index b52d8d37fa1a56473e877640bdea31b0da368e5d..aa3222545c7e4c8b3673b041e26d8b1004595853 100644 GIT binary patch literal 60332 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

1zwkHRK1QcqNW7M0#dcAzi&0oN)_Web9fZYLU|U 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 R-%1TJEfU9Xxxwo&RF| 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 Date: Sun, 21 Feb 2021 18:36:34 +0100 Subject: [PATCH 12/77] Reorganize barcode tab of edit view --- CHANGELOG.md | 1 + .../card_locker/LoyaltyCardEditActivity.java | 9 --------- .../res/layout/loyalty_card_edit_activity.xml | 19 ++++++++++++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1c65227..8c78c7af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes: - Add balance support +- Reorganize barcode tab of edit view ## v1.8.1 (2021-02-12) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index dd7e7606b..7e922facc 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -33,16 +33,13 @@ import android.view.ViewTreeObserver; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; -import android.widget.CalendarView; import android.widget.DatePicker; import android.widget.EditText; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.tabs.TabLayout; -import com.google.android.material.textfield.TextInputLayout; import com.google.zxing.BarcodeFormat; import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; @@ -50,14 +47,10 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; import java.io.InvalidObjectException; import java.math.BigDecimal; import java.text.DateFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.Currency; import java.util.Date; import java.util.HashMap; @@ -77,7 +70,6 @@ public class LoyaltyCardEditActivity extends AppCompatActivity AutoCompleteTextView expiryField; EditText balanceField; AutoCompleteTextView balanceCurrencyField; - View cardAndBarcodeLayout; TextView cardIdFieldView; AutoCompleteTextView barcodeTypeField; ImageView barcodeImage; @@ -151,7 +143,6 @@ public class LoyaltyCardEditActivity extends AppCompatActivity expiryField = findViewById(R.id.expiryField); balanceField = findViewById(R.id.balanceField); balanceCurrencyField = findViewById(R.id.balanceCurrencyField); - cardAndBarcodeLayout = findViewById(R.id.cardAndBarcodeLayout); cardIdFieldView = findViewById(R.id.cardIdView); barcodeTypeField = findViewById(R.id.barcodeTypeField); barcodeImage = findViewById(R.id.barcode); diff --git a/app/src/main/res/layout/loyalty_card_edit_activity.xml b/app/src/main/res/layout/loyalty_card_edit_activity.xml index ae421711c..50f850b63 100644 --- a/app/src/main/res/layout/loyalty_card_edit_activity.xml +++ b/app/src/main/res/layout/loyalty_card_edit_activity.xml @@ -190,6 +190,7 @@ android:paddingTop="@dimen/inputPadding" android:orientation="horizontal"> + - + + + android:orientation="horizontal"> + + + + Date: Mon, 22 Feb 2021 20:21:24 +0100 Subject: [PATCH 13/77] Simplify balance parsing logic --- .../card_locker/LoyaltyCardEditActivity.java | 20 ++++------- .../main/java/protect/card_locker/Utils.java | 19 +++++----- .../java/protect/card_locker/UtilsTest.java | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 app/src/test/java/protect/card_locker/UtilsTest.java diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 7e922facc..d5b2fc853 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -214,11 +214,11 @@ public class LoyaltyCardEditActivity extends AppCompatActivity hasChanged = true; try { - BigDecimal balance = Utils.parseCurrencyInUserLocale(s.toString()); + BigDecimal balance = Utils.parseCurrency(s.toString()); validBalance = true; balanceField.setTag(balance); - } catch (ParseException | NumberFormatException e) { + } catch (NumberFormatException e) { validBalance = false; e.printStackTrace(); } @@ -500,13 +500,10 @@ public class LoyaltyCardEditActivity extends AppCompatActivity break; } } - chip.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - hasChanged = true; + chip.setOnTouchListener((v, event) -> { + hasChanged = true; - return false; - } + return false; }); groupsChips.addView(chip); @@ -565,12 +562,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity barcodeTypeField.setAdapter(barcodeAdapter); FloatingActionButton saveButton = findViewById(R.id.fabSave); - saveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - doSave(); - } - }); + saveButton.setOnClickListener(v -> doSave()); generateIcon(storeFieldEdit.getText().toString()); } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index abbfb2edb..468555e8c 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -13,6 +13,7 @@ import java.util.Calendar; import java.util.Currency; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; import java.util.Locale; import androidx.core.graphics.ColorUtils; @@ -111,15 +112,17 @@ public class Utils { return numberFormat.format(value); } - static public BigDecimal parseCurrencyInUserLocale(String value) throws ParseException, NumberFormatException { - // BigDecimal only likes to parse in US locale - // So we have to translate whatever the input was to US locale - NumberFormat numberInputFormat = NumberFormat.getNumberInstance(); - NumberFormat numberToBigDecimalFormat = NumberFormat.getNumberInstance(Locale.US); + static public BigDecimal parseCurrency(String value) throws NumberFormatException { + // There are many ways users can write a currency, so we fix it up a bit + // 1. Replace all commas with dots + value = value.replace(',', '.'); - // BigDecimal won't understand values like 1,000 instead of 1000 - numberToBigDecimalFormat.setGroupingUsed(false); + // 2. Remove all but the last dot + while (value.split("\\.").length > 2) { + value = value.replaceFirst("\\.", ""); + } - return new BigDecimal(numberToBigDecimalFormat.format(numberInputFormat.parse(value))); + // Parse as BigDecimal + return new BigDecimal(value); } } diff --git a/app/src/test/java/protect/card_locker/UtilsTest.java b/app/src/test/java/protect/card_locker/UtilsTest.java new file mode 100644 index 000000000..d38b766e4 --- /dev/null +++ b/app/src/test/java/protect/card_locker/UtilsTest.java @@ -0,0 +1,36 @@ +package protect.card_locker; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 23) +public class UtilsTest +{ + @Test + public void parseBalances() + { + assertEquals("1000.00", Utils.parseCurrency("1.000.00").toPlainString()); + assertEquals("1000.50", Utils.parseCurrency("1.000.50").toPlainString()); + assertEquals("1000.50", Utils.parseCurrency("1.000,50").toPlainString()); + assertEquals("1000.50", Utils.parseCurrency("1,000,50").toPlainString()); + assertEquals("1000.50", Utils.parseCurrency("1,000.50").toPlainString()); + assertEquals("1000", Utils.parseCurrency("1000").toPlainString()); + assertEquals("995", Utils.parseCurrency("995").toPlainString()); + assertEquals("9.95", Utils.parseCurrency("9.95").toPlainString()); + assertEquals("9.95", Utils.parseCurrency("9,95").toPlainString()); + + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("")); + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(".")); + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(",")); + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(".,")); + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("a")); + assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("......................")); + } +} \ No newline at end of file From b4a992abf8b96734114a45a5b8b9386ee2292a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 21 Feb 2021 17:47:03 +0000 Subject: [PATCH 14/77] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.4% (126 of 128 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 170d09ddf..19c1ec8d9 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -113,4 +113,10 @@ Utløpt: %s Sentrer strekkoden på skjermen Flytt strekkoden til toppen av skjermen + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> ser ikke ut til å være en gyldig saldo. + Poeng + Valuta + Saldo + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> poeng + Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> \ No newline at end of file From 8ac2fe575c47713f9aa693a9475d1101ab5a9f74 Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Sun, 21 Feb 2021 20:40:36 +0000 Subject: [PATCH 15/77] Translated using Weblate (Dutch) Currently translated at 100.0% (128 of 128 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 700f63c87..d84906e62 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -113,4 +113,10 @@ Verlopen: %s Barcode verplaatsen naar midden van scherm Barcode verplaatsen naar bovenkant van scherm + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> lijkt geen geldig saldo te zijn. + Aantal punten + Valuta + Saldo + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> punten + Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> \ No newline at end of file From 6c53b8e9ca61f00a17666e1447dcd9097976f1f7 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 22 Feb 2021 21:54:13 +0100 Subject: [PATCH 16/77] Fix incorrect escaping from Weblate --- app/src/main/res/values-nb-rNO/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 19c1ec8d9..ccdbd22e9 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -113,10 +113,10 @@ Utløpt: %s Sentrer strekkoden på skjermen Flytt strekkoden til toppen av skjermen - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> ser ikke ut til å være en gyldig saldo. + %s ser ikke ut til å være en gyldig saldo. Poeng Valuta Saldo - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> poeng - Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - \ No newline at end of file + %s poeng + Saldo: %s + From 3fffcdbd67ed592bb75595bef77ec991a3147065 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 22 Feb 2021 21:56:20 +0100 Subject: [PATCH 17/77] Fix incorrect escaping from Weblate --- app/src/main/res/values-nl/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d84906e62..0e6158c74 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -113,10 +113,10 @@ Verlopen: %s Barcode verplaatsen naar midden van scherm Barcode verplaatsen naar bovenkant van scherm - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> lijkt geen geldig saldo te zijn. + %s lijkt geen geldig saldo te zijn. Aantal punten Valuta Saldo - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> punten - Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - \ No newline at end of file + %s punten + Saldo: %s + From d2f5cd05b575a6fcff62488dcc79742623e38850 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 22 Feb 2021 23:01:24 +0100 Subject: [PATCH 18/77] Release 1.9 --- CHANGELOG.md | 2 +- app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c78c7af7..4a9af34e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## v1.9 (2021-02-22) Changes: diff --git a/app/build.gradle b/app/build.gradle index 829169df6..2a4bdfaac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 58 - versionName "1.8.1" + versionCode 59 + versionName "1.9" vectorDrawables.useSupportLibrary true } From b265fadec3a39606a9aeb1f869bab7fd671db0f6 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 23 Feb 2021 18:48:49 +0100 Subject: [PATCH 19/77] Improve currency parsing logic --- CHANGELOG.md | 6 ++ app/build.gradle | 4 +- .../card_locker/LoyaltyCardEditActivity.java | 2 +- .../main/java/protect/card_locker/Utils.java | 16 +++- .../java/protect/card_locker/UtilsTest.java | 85 +++++++++++++++---- 5 files changed, 94 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9af34e5..94740dec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.9.1 (2021-02-23) + +Changes: + +- Improve balance parsing logic + ## v1.9 (2021-02-22) Changes: diff --git a/app/build.gradle b/app/build.gradle index 2a4bdfaac..3340fda43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 59 - versionName "1.9" + versionCode 60 + versionName "1.9.1" vectorDrawables.useSupportLibrary true } diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index d5b2fc853..33ae5fcc0 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -214,7 +214,7 @@ public class LoyaltyCardEditActivity extends AppCompatActivity hasChanged = true; try { - BigDecimal balance = Utils.parseCurrency(s.toString()); + BigDecimal balance = Utils.parseCurrency(s.toString(), Utils.currencyHasDecimals((Currency) balanceCurrencyField.getTag())); validBalance = true; balanceField.setTag(balance); diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 468555e8c..c045ff900 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -112,7 +112,21 @@ public class Utils { return numberFormat.format(value); } - static public BigDecimal parseCurrency(String value) throws NumberFormatException { + static public Boolean currencyHasDecimals(Currency currency) { + if (currency == null) { + return false; + } + + return currency.getDefaultFractionDigits() != 0; + } + + static public BigDecimal parseCurrency(String value, Boolean hasDecimals) throws NumberFormatException { + // If there are no decimals expected, remove all separators before parsing + if (!hasDecimals) { + value = value.replaceAll("\\.", "").replaceAll(",", ""); + return new BigDecimal(value); + } + // There are many ways users can write a currency, so we fix it up a bit // 1. Replace all commas with dots value = value.replace(',', '.'); diff --git a/app/src/test/java/protect/card_locker/UtilsTest.java b/app/src/test/java/protect/card_locker/UtilsTest.java index d38b766e4..f4df8773d 100644 --- a/app/src/test/java/protect/card_locker/UtilsTest.java +++ b/app/src/test/java/protect/card_locker/UtilsTest.java @@ -5,6 +5,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.util.Util; + +import java.math.BigDecimal; +import java.util.Currency; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; @@ -16,21 +20,72 @@ public class UtilsTest @Test public void parseBalances() { - assertEquals("1000.00", Utils.parseCurrency("1.000.00").toPlainString()); - assertEquals("1000.50", Utils.parseCurrency("1.000.50").toPlainString()); - assertEquals("1000.50", Utils.parseCurrency("1.000,50").toPlainString()); - assertEquals("1000.50", Utils.parseCurrency("1,000,50").toPlainString()); - assertEquals("1000.50", Utils.parseCurrency("1,000.50").toPlainString()); - assertEquals("1000", Utils.parseCurrency("1000").toPlainString()); - assertEquals("995", Utils.parseCurrency("995").toPlainString()); - assertEquals("9.95", Utils.parseCurrency("9.95").toPlainString()); - assertEquals("9.95", Utils.parseCurrency("9,95").toPlainString()); + assertEquals("1", Utils.parseCurrency("1", false).toPlainString()); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("")); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(".")); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(",")); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency(".,")); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("a")); - assertThrows(NumberFormatException.class, () -> Utils.parseCurrency("......................")); + assertEquals("1", Utils.parseCurrency("1", true).toPlainString()); + assertEquals("1.00", Utils.parseCurrency("1.00", true).toPlainString()); + assertEquals("1.00", Utils.parseCurrency("1,00", true).toPlainString()); + + assertEquals("25", Utils.parseCurrency("2.5", false).toPlainString()); + assertEquals("25", Utils.parseCurrency("2,5", false).toPlainString()); + assertEquals("205", Utils.parseCurrency("2.05", false).toPlainString()); + assertEquals("205", Utils.parseCurrency("2,05", false).toPlainString()); + + assertEquals("2.5", Utils.parseCurrency("2.5", true).toPlainString()); + assertEquals("2.5", Utils.parseCurrency("2,5", true).toPlainString()); + assertEquals("2.05", Utils.parseCurrency("2.05", true).toPlainString()); + assertEquals("2.05", Utils.parseCurrency("2,05", true).toPlainString()); + assertEquals("2.50", Utils.parseCurrency("2.50", true).toPlainString()); + assertEquals("2.50", Utils.parseCurrency("2,50", true).toPlainString()); + + assertEquals("995", Utils.parseCurrency("9.95", false).toPlainString()); + assertEquals("995", Utils.parseCurrency("9,95", false).toPlainString()); + + assertEquals("9.95", Utils.parseCurrency("9.95", true).toPlainString()); + assertEquals("9.95", Utils.parseCurrency("9,95", true).toPlainString()); + + assertEquals("1234", Utils.parseCurrency("1234", false).toPlainString()); + assertEquals("1234", Utils.parseCurrency("1.234", false).toPlainString()); + assertEquals("1234", Utils.parseCurrency("1,234", false).toPlainString()); + + assertEquals("1234", Utils.parseCurrency("1234", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1234.00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1.234.00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1.234,00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1,234.00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1,234,00", true).toPlainString()); + } + + @Test + public void formatBalances() + { + Currency euro = Currency.getInstance("EUR"); + + assertEquals("1", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1"), null)); + assertEquals("1.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1"), euro)); + + assertEquals("25", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("25"), null)); + assertEquals("25.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("25"), euro)); + + assertEquals("2", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.5"), null)); + assertEquals("2.50", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.5"), euro)); + + assertEquals("2", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.05"), null)); + assertEquals("2.05", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.05"), euro)); + + assertEquals("2", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.50"), null)); + assertEquals("2.50", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("2.50"), euro)); + + assertEquals("995", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("995"), null)); + assertEquals("995.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("995"), euro)); + + assertEquals("10", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("9.95"), null)); + assertEquals("9.95", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("9.95"), euro)); + + assertEquals("1,234", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234"), null)); + assertEquals("1,234.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234"), euro)); + + assertEquals("1,234", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234.00"), null)); + assertEquals("1,234.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234.00"), euro)); } } \ No newline at end of file From bc808d30452b9600044b26c439b4708d088b965f Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 23 Feb 2021 20:55:01 +0100 Subject: [PATCH 20/77] Really fix --- CHANGELOG.md | 1 + app/build.gradle | 2 +- .../card_locker/LoyaltyCardEditActivity.java | 22 +++++++++++-------- .../main/java/protect/card_locker/Utils.java | 2 ++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94740dec9..2bc678e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes: - Improve balance parsing logic +- Fix currency decimal display on main screen ## v1.9 (2021-02-22) diff --git a/app/build.gradle b/app/build.gradle index 3340fda43..b24251fdb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 60 + versionCode 61 versionName "1.9.1" vectorDrawables.useSupportLibrary true diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 33ae5fcc0..7bf92e5c4 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -246,7 +246,11 @@ public class LoyaltyCardEditActivity extends AppCompatActivity balanceCurrencyField.setTag(currency); - balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol((BigDecimal) balanceField.getTag(), currency)); + BigDecimal balance = (BigDecimal) balanceField.getTag(); + + if (balance != null) { + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(balance, currency)); + } } @Override @@ -366,10 +370,10 @@ public class LoyaltyCardEditActivity extends AppCompatActivity noteFieldEdit.setText(""); expiryField.setTag(null); expiryField.setText(""); - balanceField.setTag(null); - balanceField.setText(""); balanceCurrencyField.setTag(null); balanceCurrencyField.setText(""); + balanceField.setTag(null); + balanceField.setText(""); cardIdFieldView.setText(""); barcodeTypeField.setText(""); } @@ -409,18 +413,18 @@ public class LoyaltyCardEditActivity extends AppCompatActivity formatExpiryField(loyaltyCard.expiry); } - if(balanceField.getText().length() == 0) - { - balanceField.setTag(loyaltyCard.balance); - balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(loyaltyCard.balance, loyaltyCard.balanceType)); - } - if(balanceCurrencyField.getText().length() == 0) { balanceCurrencyField.setTag(loyaltyCard.balanceType); formatBalanceCurrencyField(loyaltyCard.balanceType); } + if(balanceField.getText().length() == 0) + { + balanceField.setTag(loyaltyCard.balance); + balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(loyaltyCard.balance, loyaltyCard.balanceType)); + } + if(cardIdFieldView.getText().length() == 0) { cardIdFieldView.setText(loyaltyCard.cardId); diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index c045ff900..a6ba67032 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -94,6 +94,8 @@ public class Utils { NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(); currencyFormat.setCurrency(currency); + currencyFormat.setMinimumFractionDigits(currency.getDefaultFractionDigits()); + currencyFormat.setMaximumFractionDigits(currency.getDefaultFractionDigits()); return currencyFormat.format(value); } From 1c92787254eb86ba6db34ccfcc1623435d9994de Mon Sep 17 00:00:00 2001 From: inesre Date: Mon, 22 Feb 2021 23:01:50 +0000 Subject: [PATCH 21/77] Translated using Weblate (Spanish) Currently translated at 94.5% (121 of 128 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/ --- app/src/main/res/values-es/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 04069fc5c..b6a18d5ee 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -103,4 +103,14 @@ %d tarjeta %d tarjetas - + Puntos + Centrar el código de barras en la pantalla + Mover el código de barras a la zona superior de la pantalla + Elegir fecha de caducidad + Nunca + Fecha de caducidad + Editar el código de barras + Código de barras + Tarjeta + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> puntos + \ No newline at end of file From d03d96b194a066c779ceb49c48a1767083a3598b Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 24 Feb 2021 12:28:04 +0100 Subject: [PATCH 22/77] Fix Weblate incorrect escaping --- app/src/main/res/values-es/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b6a18d5ee..d1c9a4992 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -112,5 +112,5 @@ Editar el código de barras Código de barras Tarjeta - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> puntos - \ No newline at end of file + %s puntos + From efc8e6ae33a033b4dd817f848126a9b8f897d3a3 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 24 Feb 2021 19:17:16 +0100 Subject: [PATCH 23/77] Fix parsing balance for countries using space as separator --- CHANGELOG.md | 6 ++++++ app/build.gradle | 4 ++-- app/src/main/java/protect/card_locker/Utils.java | 6 +++--- .../test/java/protect/card_locker/UtilsTest.java | 15 ++++++++++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc678e39..5c7ceefa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.9.2 (2021-02-24) + +Changes: + +- Fix parsing balance for countries using space as separator + ## v1.9.1 (2021-02-23) Changes: diff --git a/app/build.gradle b/app/build.gradle index b24251fdb..a7d9df8c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 61 - versionName "1.9.1" + versionCode 62 + versionName "1.9.2" vectorDrawables.useSupportLibrary true } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index a6ba67032..287816965 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -125,13 +125,13 @@ public class Utils { static public BigDecimal parseCurrency(String value, Boolean hasDecimals) throws NumberFormatException { // If there are no decimals expected, remove all separators before parsing if (!hasDecimals) { - value = value.replaceAll("\\.", "").replaceAll(",", ""); + value = value.replaceAll("[^0-9]", ""); return new BigDecimal(value); } // There are many ways users can write a currency, so we fix it up a bit - // 1. Replace all commas with dots - value = value.replace(',', '.'); + // 1. Replace all non-numbers with dots + value = value.replaceAll("[^0-9]", "."); // 2. Remove all but the last dot while (value.split("\\.").length > 2) { diff --git a/app/src/test/java/protect/card_locker/UtilsTest.java b/app/src/test/java/protect/card_locker/UtilsTest.java index f4df8773d..761633feb 100644 --- a/app/src/test/java/protect/card_locker/UtilsTest.java +++ b/app/src/test/java/protect/card_locker/UtilsTest.java @@ -25,28 +25,37 @@ public class UtilsTest assertEquals("1", Utils.parseCurrency("1", true).toPlainString()); assertEquals("1.00", Utils.parseCurrency("1.00", true).toPlainString()); assertEquals("1.00", Utils.parseCurrency("1,00", true).toPlainString()); + assertEquals("1.00", Utils.parseCurrency("1 00", true).toPlainString()); assertEquals("25", Utils.parseCurrency("2.5", false).toPlainString()); assertEquals("25", Utils.parseCurrency("2,5", false).toPlainString()); + assertEquals("25", Utils.parseCurrency("2 5", false).toPlainString()); assertEquals("205", Utils.parseCurrency("2.05", false).toPlainString()); assertEquals("205", Utils.parseCurrency("2,05", false).toPlainString()); + assertEquals("205", Utils.parseCurrency("2 05", false).toPlainString()); assertEquals("2.5", Utils.parseCurrency("2.5", true).toPlainString()); assertEquals("2.5", Utils.parseCurrency("2,5", true).toPlainString()); + assertEquals("2.5", Utils.parseCurrency("2 5", true).toPlainString()); assertEquals("2.05", Utils.parseCurrency("2.05", true).toPlainString()); assertEquals("2.05", Utils.parseCurrency("2,05", true).toPlainString()); + assertEquals("2.05", Utils.parseCurrency("2 05", true).toPlainString()); assertEquals("2.50", Utils.parseCurrency("2.50", true).toPlainString()); assertEquals("2.50", Utils.parseCurrency("2,50", true).toPlainString()); + assertEquals("2.50", Utils.parseCurrency("2 50", true).toPlainString()); assertEquals("995", Utils.parseCurrency("9.95", false).toPlainString()); assertEquals("995", Utils.parseCurrency("9,95", false).toPlainString()); + assertEquals("995", Utils.parseCurrency("9 95", false).toPlainString()); assertEquals("9.95", Utils.parseCurrency("9.95", true).toPlainString()); assertEquals("9.95", Utils.parseCurrency("9,95", true).toPlainString()); + assertEquals("9.95", Utils.parseCurrency("9 95", true).toPlainString()); assertEquals("1234", Utils.parseCurrency("1234", false).toPlainString()); assertEquals("1234", Utils.parseCurrency("1.234", false).toPlainString()); assertEquals("1234", Utils.parseCurrency("1,234", false).toPlainString()); + assertEquals("1234", Utils.parseCurrency("1 234", false).toPlainString()); assertEquals("1234", Utils.parseCurrency("1234", true).toPlainString()); assertEquals("1234.00", Utils.parseCurrency("1234.00", true).toPlainString()); @@ -54,6 +63,10 @@ public class UtilsTest assertEquals("1234.00", Utils.parseCurrency("1.234,00", true).toPlainString()); assertEquals("1234.00", Utils.parseCurrency("1,234.00", true).toPlainString()); assertEquals("1234.00", Utils.parseCurrency("1,234,00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1 234,00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1 234,00", true).toPlainString()); + assertEquals("1234.00", Utils.parseCurrency("1 234 00", true).toPlainString()); + } @Test @@ -88,4 +101,4 @@ public class UtilsTest assertEquals("1,234", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234.00"), null)); assertEquals("1,234.00", Utils.formatBalanceWithoutCurrencySymbol(new BigDecimal("1234.00"), euro)); } -} \ No newline at end of file +} From b558d2178a08c763763406d0a46ad469fc3d7a85 Mon Sep 17 00:00:00 2001 From: Simone Dotto Date: Wed, 24 Feb 2021 13:35:12 +0000 Subject: [PATCH 24/77] Translated using Weblate (Italian) Currently translated at 100.0% (128 of 128 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/ --- app/src/main/res/values-it/strings.xml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 81e834f79..6ced08af2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -103,4 +103,20 @@ %d carta %d carte - + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> non sembra un saldo corretto. + Punti + Valuta + Saldo + Centra il codice a barre sullo schermo + Sposta il codice a barre in cima allo schermo + Scegli data di scadenza + Mai + Data di scadenza + Modifica il codice a barre + Codice a barre + Carta + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> punti + Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Scaduta: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Scade: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + \ No newline at end of file From 01bd91ff661f8ef4fb05c1739b8f0adfc2156df5 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 25 Feb 2021 19:54:46 +0100 Subject: [PATCH 25/77] Fix Weblate incorrectly escaping XML --- app/src/main/res/values-it/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6ced08af2..e854bfad5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -115,8 +115,8 @@ Modifica il codice a barre Codice a barre Carta - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> punti - Saldo: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Scaduta: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Scade: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - \ No newline at end of file + %s punti + Saldo: %s + Scaduta: %s + Scade: %s + From db22703ec0e62555dc49e3d65d877e9f25a1a440 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 1 Mar 2021 23:02:03 +0100 Subject: [PATCH 26/77] Create Voucher Vault import code and test --- .../java/protect/card_locker/DBHelper.java | 6 +- .../protect/card_locker/ImportExportTask.java | 3 + .../CsvDatabaseExporter.java | 6 +- .../CsvDatabaseImporter.java | 6 +- .../{ => importexport}/DatabaseExporter.java | 4 +- .../{ => importexport}/DatabaseImporter.java | 10 +- .../MultiFormatExporter.java | 7 +- .../MultiFormatImporter.java | 13 +- .../importexport/VoucherVaultImporter.java | 130 +++++++++++++++ .../protect/card_locker/ImportExportTest.java | 148 ++++++++++++------ 10 files changed, 276 insertions(+), 57 deletions(-) rename app/src/main/java/protect/card_locker/{ => importexport}/CsvDatabaseExporter.java (95%) rename app/src/main/java/protect/card_locker/{ => importexport}/CsvDatabaseImporter.java (98%) rename app/src/main/java/protect/card_locker/{ => importexport}/DatabaseExporter.java (83%) rename app/src/main/java/protect/card_locker/{ => importexport}/DatabaseImporter.java (60%) rename app/src/main/java/protect/card_locker/{ => importexport}/MultiFormatExporter.java (85%) rename app/src/main/java/protect/card_locker/{ => importexport}/MultiFormatImporter.java (76%) create mode 100644 app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 10a631eca..ce27a76cf 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -20,14 +20,14 @@ public class DBHelper extends SQLiteOpenHelper public static final int ORIGINAL_DATABASE_VERSION = 1; public static final int DATABASE_VERSION = 8; - static class LoyaltyCardDbGroups + public static class LoyaltyCardDbGroups { public static final String TABLE = "groups"; public static final String ID = "_id"; public static final String ORDER = "orderId"; } - static class LoyaltyCardDbIds + public static class LoyaltyCardDbIds { public static final String TABLE = "cards"; public static final String ID = "_id"; @@ -43,7 +43,7 @@ public class DBHelper extends SQLiteOpenHelper public static final String STAR_STATUS = "starstatus"; } - static class LoyaltyCardDbIdsGroups + public static class LoyaltyCardDbIdsGroups { public static final String TABLE = "cardsGroups"; public static final String cardID = "cardId"; diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index 8537fbe99..402d57181 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -13,6 +13,9 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; +import protect.card_locker.importexport.MultiFormatExporter; +import protect.card_locker.importexport.MultiFormatImporter; + class ImportExportTask extends AsyncTask { private static final String TAG = "Catima"; diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java similarity index 95% rename from app/src/main/java/protect/card_locker/CsvDatabaseExporter.java rename to app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java index dd8a3037c..0d58f4403 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseExporter.java @@ -1,4 +1,4 @@ -package protect.card_locker; +package protect.card_locker.importexport; import android.database.Cursor; @@ -8,6 +8,10 @@ import org.apache.commons.csv.CSVPrinter; import java.io.IOException; import java.io.OutputStreamWriter; +import protect.card_locker.DBHelper; +import protect.card_locker.Group; +import protect.card_locker.LoyaltyCard; + /** * Class for exporting the database into CSV (Comma Separate Values) * format. diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java similarity index 98% rename from app/src/main/java/protect/card_locker/CsvDatabaseImporter.java rename to app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java index 5ba74393e..c2491bc42 100644 --- a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java @@ -1,4 +1,4 @@ -package protect.card_locker; +package protect.card_locker.importexport; import android.database.sqlite.SQLiteDatabase; @@ -15,6 +15,10 @@ import java.util.Currency; import java.util.Date; import java.util.List; +import protect.card_locker.DBHelper; +import protect.card_locker.FormatException; +import protect.card_locker.Group; + /** * Class for importing a database from CSV (Comma Separate Values) * formatted data. diff --git a/app/src/main/java/protect/card_locker/DatabaseExporter.java b/app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java similarity index 83% rename from app/src/main/java/protect/card_locker/DatabaseExporter.java rename to app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java index 94e4949d6..a12a124af 100644 --- a/app/src/main/java/protect/card_locker/DatabaseExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/DatabaseExporter.java @@ -1,8 +1,10 @@ -package protect.card_locker; +package protect.card_locker.importexport; import java.io.IOException; import java.io.OutputStreamWriter; +import protect.card_locker.DBHelper; + /** * Interface for a class which can export the contents of the database * in a given format. diff --git a/app/src/main/java/protect/card_locker/DatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java similarity index 60% rename from app/src/main/java/protect/card_locker/DatabaseImporter.java rename to app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java index 33d2797bf..867454385 100644 --- a/app/src/main/java/protect/card_locker/DatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java @@ -1,7 +1,13 @@ -package protect.card_locker; +package protect.card_locker.importexport; + +import org.json.JSONException; import java.io.IOException; import java.io.InputStreamReader; +import java.text.ParseException; + +import protect.card_locker.DBHelper; +import protect.card_locker.FormatException; /** * Interface for a class which can import the contents of a stream @@ -15,5 +21,5 @@ public interface DatabaseImporter * @throws IOException * @throws FormatException */ - void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException; + void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException, JSONException, ParseException; } diff --git a/app/src/main/java/protect/card_locker/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java similarity index 85% rename from app/src/main/java/protect/card_locker/MultiFormatExporter.java rename to app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index 5298e2ad7..45f702b4d 100644 --- a/app/src/main/java/protect/card_locker/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -1,10 +1,15 @@ -package protect.card_locker; +package protect.card_locker.importexport; import android.util.Log; import java.io.IOException; import java.io.OutputStreamWriter; +import protect.card_locker.DBHelper; +import protect.card_locker.DataFormat; +import protect.card_locker.importexport.CsvDatabaseExporter; +import protect.card_locker.importexport.DatabaseExporter; + public class MultiFormatExporter { private static final String TAG = "Catima"; diff --git a/app/src/main/java/protect/card_locker/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java similarity index 76% rename from app/src/main/java/protect/card_locker/MultiFormatImporter.java rename to app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 6a27af7e4..62d5af905 100644 --- a/app/src/main/java/protect/card_locker/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -1,9 +1,18 @@ -package protect.card_locker; +package protect.card_locker.importexport; import android.util.Log; +import org.json.JSONException; + import java.io.IOException; import java.io.InputStreamReader; +import java.text.ParseException; + +import protect.card_locker.DBHelper; +import protect.card_locker.DataFormat; +import protect.card_locker.FormatException; +import protect.card_locker.importexport.CsvDatabaseImporter; +import protect.card_locker.importexport.DatabaseImporter; public class MultiFormatImporter { @@ -38,7 +47,7 @@ public class MultiFormatImporter importer.importData(db, input); return true; } - catch(IOException | FormatException | InterruptedException e) + catch(IOException | FormatException | InterruptedException | JSONException | ParseException e) { Log.e(TAG, "Failed to import data", e); } diff --git a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java new file mode 100644 index 000000000..837cddc59 --- /dev/null +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -0,0 +1,130 @@ +package protect.card_locker.importexport; + +import android.annotation.SuppressLint; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; + +import com.google.zxing.BarcodeFormat; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Currency; +import java.util.Date; + +import protect.card_locker.DBHelper; +import protect.card_locker.FormatException; + +/** + * Class for importing a database from CSV (Comma Separate Values) + * formatted data. + * + * The database's loyalty cards are expected to appear in the CSV data. + * A header is expected for the each table showing the names of the columns. + */ +public class VoucherVaultImporter implements DatabaseImporter +{ + public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, JSONException, ParseException { + BufferedReader bufferedReader = new BufferedReader(input); + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + sb.append(line); + } + JSONArray jsonArray = new JSONArray(sb.toString()); + + SQLiteDatabase database = db.getWritableDatabase(); + database.beginTransaction(); + + // See https://github.com/tim-smart/vouchervault/issues/4#issuecomment-788226503 for more info + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonCard = jsonArray.getJSONObject(i); + + String store = jsonCard.getString("description"); + + Date expiry = null; + if (!jsonCard.isNull("expires")) { + @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + expiry = dateFormat.parse(jsonCard.getString("expires")); + } + + BigDecimal balance; + if (!jsonCard.isNull("balance")) { + balance = new BigDecimal(String.valueOf(jsonCard.getDouble("balance"))); + } else { + balance = new BigDecimal("0"); + } + + Currency balanceType = Currency.getInstance("USD"); + + String cardId = jsonCard.getString("code"); + + String barcodeType = null; + + String codeTypeFromJSON = jsonCard.getString("codeType"); + switch (codeTypeFromJSON) { + case "CODE128": + barcodeType = BarcodeFormat.CODE_128.name(); + break; + case "CODE39": + barcodeType = BarcodeFormat.CODE_39.name(); + break; + case "EAN13": + barcodeType = BarcodeFormat.EAN_13.name(); + break; + case "QR": + barcodeType = BarcodeFormat.QR_CODE.name(); + break; + case "TEXT": + break; + default: + throw new FormatException("Unknown barcode type found: " + codeTypeFromJSON); + } + + int headerColor; + + String colorFromJSON = jsonCard.getString("color"); + switch (colorFromJSON) { + case "GREY": + headerColor = Color.GRAY; + break; + case "BLUE": + headerColor = Color.BLUE; + break; + case "GREEN": + headerColor = Color.GREEN; + break; + case "ORANGE": + headerColor = Color.rgb(255, 165, 0); + break; + case "PURPLE": + headerColor = Color.rgb(128, 0, 128); + break; + case "RED": + headerColor = Color.RED; + break; + case "YELLOW": + headerColor = Color.YELLOW; + break; + default: + throw new FormatException("Unknown colour type foun: " + colorFromJSON); + } + + db.insertLoyaltyCard(store, "", expiry, balance, balanceType, cardId, barcodeType, headerColor, 0); + } + + database.setTransactionSuccessful(); + database.endTransaction(); + database.close(); + + bufferedReader.close(); + } +} \ No newline at end of file diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 2baf9ee73..e31e048ea 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -3,10 +3,12 @@ package protect.card_locker; import android.app.Activity; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; import android.os.Environment; import com.google.zxing.BarcodeFormat; +import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,11 +28,17 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; +import java.util.Currency; import java.util.Date; import java.util.List; +import protect.card_locker.importexport.MultiFormatExporter; +import protect.card_locker.importexport.MultiFormatImporter; +import protect.card_locker.importexport.VoucherVaultImporter; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -112,20 +120,65 @@ public class ImportExportTest boolean result = (id != -1); assertTrue(result); + LoyaltyCard card = db.getLoyaltyCard((int) id); + assertEquals("No Expiry", card.store); + assertEquals("", card.note); + assertEquals(null, card.expiry); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(BARCODE_TYPE, card.barcodeType); + assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(0, card.starStatus); + id = db.insertLoyaltyCard("Past", "", new Date((long) 1), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); + card = db.getLoyaltyCard((int) id); + assertEquals("Past", card.store); + assertEquals("", card.note); + assertTrue(card.expiry.before(new Date())); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(BARCODE_TYPE, card.barcodeType); + assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(0, card.starStatus); + id = db.insertLoyaltyCard("Today", "", new Date(), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); + card = db.getLoyaltyCard((int) id); + assertEquals("Today", card.store); + assertEquals("", card.note); + assertTrue(card.expiry.before(new Date(new Date().getTime()+86400))); + assertTrue(card.expiry.after(new Date(new Date().getTime()-86400))); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(BARCODE_TYPE, card.barcodeType); + assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(0, card.starStatus); + // This will break after 19 January 2038 // If someone is still maintaining this code base by then: I love you - id = db.insertLoyaltyCard("Future", "", new Date(2147483648L), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); + id = db.insertLoyaltyCard("Future", "", new Date(2147483648000L), new BigDecimal("0"), null, BARCODE_DATA, BARCODE_TYPE, 0, 0); result = (id != -1); assertTrue(result); + card = db.getLoyaltyCard((int) id); + assertEquals("Future", card.store); + assertEquals("", card.note); + assertTrue(card.expiry.after(new Date(new Date().getTime()+86400))); + assertEquals(new BigDecimal("0"), card.balance); + assertEquals(null, card.balanceType); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(BARCODE_TYPE, card.barcodeType); + assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(0, card.starStatus); + assertEquals(4, db.getLoyaltyCardCount()); } @@ -830,59 +883,62 @@ public class ImportExportTest clearDatabase(); } + @Test + public void importVoucherVault() throws IOException, FormatException, JSONException, ParseException { + String jsonText = "[\n" + + " {\n" + + " \"uuid\": \"ae1ae525-3f27-481e-853a-8c30b7fa12d8\",\n" + + " \"description\": \"Clothes Store\",\n" + + " \"code\": \"123456\",\n" + + " \"codeType\": \"CODE128\",\n" + + " \"expires\": null,\n" + + " \"removeOnceExpired\": true,\n" + + " \"balance\": null,\n" + + " \"color\": \"GREY\"\n" + + " },\n" + + " {\n" + + " \"uuid\": \"29a5d3b3-eace-4311-a15c-4c7e6a010531\",\n" + + " \"description\": \"Department Store\",\n" + + " \"code\": \"26846363\",\n" + + " \"codeType\": \"CODE39\",\n" + + " \"expires\": \"2021-03-26T00:00:00.000\",\n" + + " \"removeOnceExpired\": true,\n" + + " \"balance\": 3.5,\n" + + " \"color\": \"PURPLE\"\n" + + " }\n" + + "]"; - private void checkLoyaltyCardsExpiry() - { - Cursor cursor = db.getLoyaltyCardCursor(); - cursor.moveToNext(); - LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); - assertEquals("Never", card.store); + ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonText.getBytes(StandardCharsets.UTF_8)); + InputStreamReader inStream = new InputStreamReader(inputStream); + + // Import the Voucher Vault data + new VoucherVaultImporter().importData(db, inStream); + assertEquals(2, db.getLoyaltyCardCount()); + + LoyaltyCard card = db.getLoyaltyCard(1); + + assertEquals("Clothes Store", card.store); assertEquals("", card.note); assertEquals(null, card.expiry); assertEquals(new BigDecimal("0"), card.balance); - assertEquals(null, card.balanceType); - assertEquals(BARCODE_DATA, card.cardId); - assertEquals(BARCODE_TYPE, card.barcodeType); - assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(Currency.getInstance("USD"), card.balanceType); + assertEquals("123456", card.cardId); + assertEquals(BarcodeFormat.CODE_128.name(), card.barcodeType); assertEquals(0, card.starStatus); + assertEquals(Color.GRAY, (long) card.headerColor); - cursor.moveToNext(); - card = LoyaltyCard.toLoyaltyCard(cursor); - assertEquals("Past", card.store); + card = db.getLoyaltyCard(2); + + assertEquals("Department Store", card.store); assertEquals("", card.note); - assertTrue(card.expiry.before(new Date())); - assertEquals(new BigDecimal("0"), card.balance); - assertEquals(null, card.balanceType); - assertEquals(BARCODE_DATA, card.cardId); - assertEquals(BARCODE_TYPE, card.barcodeType); - assertEquals(Integer.valueOf(0), card.headerColor); + assertEquals(new Date(1616713200000L), card.expiry); + assertEquals(new BigDecimal("3.5"), card.balance); + assertEquals(Currency.getInstance("USD"), card.balanceType); + assertEquals("26846363", card.cardId); + assertEquals(BarcodeFormat.CODE_39.name(), card.barcodeType); assertEquals(0, card.starStatus); + assertEquals(Color.rgb(128, 0, 128), (long) card.headerColor); - cursor.moveToNext(); - card = LoyaltyCard.toLoyaltyCard(cursor); - assertEquals("Today", card.store); - assertEquals("", card.note); - assertTrue(card.expiry.before(new Date(new Date().getTime()+86400))); - assertTrue(card.expiry.after(new Date(new Date().getTime()-86400))); - assertEquals(new BigDecimal("0"), card.balance); - assertEquals(null, card.balanceType); - assertEquals(BARCODE_DATA, card.cardId); - assertEquals(BARCODE_TYPE, card.barcodeType); - assertEquals(Integer.valueOf(0), card.headerColor); - assertEquals(0, card.starStatus); - - cursor.moveToNext(); - card = LoyaltyCard.toLoyaltyCard(cursor); - assertEquals("Future", card.store); - assertEquals("", card.note); - assertTrue(card.expiry.after(new Date(new Date().getTime()+86400))); - assertEquals(new BigDecimal("0"), card.balance); - assertEquals(null, card.balanceType); - assertEquals(BARCODE_DATA, card.cardId); - assertEquals(BARCODE_TYPE, card.barcodeType); - assertEquals(Integer.valueOf(0), card.headerColor); - assertEquals(0, card.starStatus); - - cursor.close(); + clearDatabase(); } } From 40de4a8dc4925460766870cf05fffebd363f9a12 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 2 Mar 2021 21:10:14 +0100 Subject: [PATCH 27/77] Fix date parsing --- .../protect/card_locker/importexport/VoucherVaultImporter.java | 2 ++ app/src/test/java/protect/card_locker/ImportExportTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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 837cddc59..317adb65a 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -18,6 +18,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Currency; import java.util.Date; +import java.util.TimeZone; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; @@ -53,6 +54,7 @@ public class VoucherVaultImporter implements DatabaseImporter Date expiry = null; if (!jsonCard.isNull("expires")) { @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); expiry = dateFormat.parse(jsonCard.getString("expires")); } diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index e31e048ea..17823f253 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -931,7 +931,7 @@ public class ImportExportTest assertEquals("Department Store", card.store); assertEquals("", card.note); - assertEquals(new Date(1616713200000L), card.expiry); + assertEquals(new Date(1616716800000L), card.expiry); assertEquals(new BigDecimal("3.5"), card.balance); assertEquals(Currency.getInstance("USD"), card.balanceType); assertEquals("26846363", card.cardId); From ffa39000f7813842410d827c987fd2e3c9d2d04e Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 4 Mar 2021 23:35:44 +0100 Subject: [PATCH 28/77] Add Voucher Vault import to UI --- CHANGELOG.md | 6 + .../java/protect/card_locker/DataFormat.java | 4 +- .../card_locker/ImportExportActivity.java | 44 ++- .../importexport/MultiFormatExporter.java | 2 +- .../importexport/MultiFormatImporter.java | 5 +- app/src/main/res/values/arrays.xml | 8 + app/src/main/res/values/strings.xml | 1 + .../protect/card_locker/ImportExportTest.java | 297 +++++++++--------- 8 files changed, 198 insertions(+), 169 deletions(-) create mode 100644 app/src/main/res/values/arrays.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7ceefa3..56951fb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +Changes: + +- Support importing [Voucher Vault](https://github.com/tim-smart/vouchervault/) exports + ## v1.9.2 (2021-02-24) Changes: diff --git a/app/src/main/java/protect/card_locker/DataFormat.java b/app/src/main/java/protect/card_locker/DataFormat.java index 722700f00..4b4b23e08 100644 --- a/app/src/main/java/protect/card_locker/DataFormat.java +++ b/app/src/main/java/protect/card_locker/DataFormat.java @@ -2,7 +2,7 @@ package protect.card_locker; public enum DataFormat { - CSV, - + Catima, + VoucherVault ; } diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index f5d89238d..cb70b396f 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -35,10 +35,12 @@ public class ImportExportActivity extends AppCompatActivity private static final int PERMISSIONS_EXTERNAL_STORAGE = 1; private static final int CHOOSE_EXPORT_LOCATION = 2; - private static final int CHOOSE_EXPORTED_FILE = 3; + private static final int IMPORT = 3; private ImportExportTask importExporter; + private DataFormat importDataFormat; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -93,7 +95,7 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(View v) { - chooseFileWithIntent(intentGetContentAction, CHOOSE_EXPORTED_FILE); + chooseImportType(intentGetContentAction); } }); @@ -106,12 +108,35 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(View v) { - chooseFileWithIntent(intentPickAction, CHOOSE_EXPORTED_FILE); + chooseImportType(intentPickAction); } }); } - private void startImport(final InputStream target, final Uri targetUri) + private void chooseImportType(Intent baseIntent) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.chooseImportType) + .setItems(R.array.import_types_array, (dialog, which) -> { + switch (which) { + // Catima + case 0: + // Loyalty Card Keychain + case 1: + importDataFormat = DataFormat.Catima; + break; + // Voucher Vault + case 2: + importDataFormat = DataFormat.VoucherVault; + break; + default: + throw new IllegalArgumentException("Unknown DataFormat"); + } + chooseFileWithIntent(baseIntent, IMPORT); + }); + builder.show(); + } + + private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat) { ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @@ -123,7 +148,7 @@ public class ImportExportActivity extends AppCompatActivity }; importExporter = new ImportExportTask(ImportExportActivity.this, - DataFormat.CSV, target, listener); + dataFormat, target, listener); importExporter.execute(); } @@ -139,7 +164,7 @@ public class ImportExportActivity extends AppCompatActivity }; importExporter = new ImportExportTask(ImportExportActivity.this, - DataFormat.CSV, target, listener); + DataFormat.Catima, target, listener); importExporter.execute(); } @@ -294,7 +319,7 @@ public class ImportExportActivity extends AppCompatActivity { super.onActivityResult(requestCode, resultCode, data); - if (resultCode != RESULT_OK || (requestCode != CHOOSE_EXPORT_LOCATION && requestCode != CHOOSE_EXPORTED_FILE)) + if (resultCode != RESULT_OK) { Log.w(TAG, "Failed onActivityResult(), result=" + resultCode); return; @@ -336,8 +361,9 @@ public class ImportExportActivity extends AppCompatActivity reader = new FileInputStream(new File(uri.toString())); } - Log.e(TAG, "Starting file export with: " + uri.toString()); - startImport(reader, uri); + Log.e(TAG, "Starting file import with: " + uri.toString()); + + startImport(reader, uri, importDataFormat); } } catch(FileNotFoundException e) diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index 45f702b4d..f6509d561 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -30,7 +30,7 @@ public class MultiFormatExporter switch(format) { - case CSV: + case Catima: exporter = new CsvDatabaseExporter(); break; } diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 62d5af905..3186440cd 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -35,9 +35,12 @@ public class MultiFormatImporter switch(format) { - case CSV: + case Catima: importer = new CsvDatabaseImporter(); break; + case VoucherVault: + importer = new VoucherVaultImporter(); + break; } if (importer != null) diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..303cc27bf --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,8 @@ + + + + Catima + Loyalty Card Keychain + Voucher Vault + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71260b998..db69108fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,4 +151,5 @@ Points %s does not seem to be a valid balance. + Import data from which app? diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 17823f253..11413b6b8 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -37,7 +37,6 @@ import java.util.List; import protect.card_locker.importexport.MultiFormatExporter; import protect.card_locker.importexport.MultiFormatImporter; -import protect.card_locker.importexport.VoucherVaultImporter; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -322,34 +321,31 @@ public class ImportExportTest { final int NUM_CARDS = 10; - for(DataFormat format : DataFormat.values()) - { - addLoyaltyCards(NUM_CARDS); + addLoyaltyCards(NUM_CARDS); - ByteArrayOutputStream outData = new ByteArrayOutputStream(); - OutputStreamWriter outStream = new OutputStreamWriter(outData); + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); - // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, format); - assertTrue(result); - outStream.close(); + // Export data to CSV format + boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + assertTrue(result); + outStream.close(); - clearDatabase(); + clearDatabase(); - ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); - // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); - assertTrue(result); + // Import the CSV data + result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + assertTrue(result); - assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); - checkLoyaltyCards(); + checkLoyaltyCards(); - // Clear the database for the next format under test - clearDatabase(); - } + // Clear the database for the next format under test + clearDatabase(); } @Test @@ -357,34 +353,31 @@ public class ImportExportTest { final int NUM_CARDS = 9; - for(DataFormat format : DataFormat.values()) - { - addLoyaltyCardsFiveStarred(); + addLoyaltyCardsFiveStarred(); - ByteArrayOutputStream outData = new ByteArrayOutputStream(); - OutputStreamWriter outStream = new OutputStreamWriter(outData); + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); - // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, format); - assertTrue(result); - outStream.close(); + // Export data to CSV format + boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + assertTrue(result); + outStream.close(); - clearDatabase(); + clearDatabase(); - ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); - // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); - assertTrue(result); + // Import the CSV data + result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + assertTrue(result); - assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); - checkLoyaltyCardsFiveStarred(); + checkLoyaltyCardsFiveStarred(); - // Clear the database for the next format under test - clearDatabase(); - } + // Clear the database for the next format under test + clearDatabase(); } private List groupsToGroupNames(List groups) @@ -404,77 +397,74 @@ public class ImportExportTest final int NUM_CARDS = 10; final int NUM_GROUPS = 3; - for(DataFormat format : DataFormat.values()) - { - addLoyaltyCards(NUM_CARDS); - addGroups(NUM_GROUPS); + addLoyaltyCards(NUM_CARDS); + addGroups(NUM_GROUPS); - List emptyGroup = new ArrayList<>(); + List emptyGroup = new ArrayList<>(); - List groupsForOne = new ArrayList<>(); - groupsForOne.add(db.getGroup("group, \" 1")); + List groupsForOne = new ArrayList<>(); + groupsForOne.add(db.getGroup("group, \" 1")); - List groupsForTwo = new ArrayList<>(); - groupsForTwo.add(db.getGroup("group, \" 1")); - groupsForTwo.add(db.getGroup("group, \" 2")); + List groupsForTwo = new ArrayList<>(); + groupsForTwo.add(db.getGroup("group, \" 1")); + groupsForTwo.add(db.getGroup("group, \" 2")); - List groupsForThree = new ArrayList<>(); - groupsForThree.add(db.getGroup("group, \" 1")); - groupsForThree.add(db.getGroup("group, \" 2")); - groupsForThree.add(db.getGroup("group, \" 3")); + List groupsForThree = new ArrayList<>(); + groupsForThree.add(db.getGroup("group, \" 1")); + groupsForThree.add(db.getGroup("group, \" 2")); + groupsForThree.add(db.getGroup("group, \" 3")); - List groupsForFour = new ArrayList<>(); - groupsForFour.add(db.getGroup("group, \" 1")); - groupsForFour.add(db.getGroup("group, \" 2")); - groupsForFour.add(db.getGroup("group, \" 3")); + List groupsForFour = new ArrayList<>(); + groupsForFour.add(db.getGroup("group, \" 1")); + groupsForFour.add(db.getGroup("group, \" 2")); + groupsForFour.add(db.getGroup("group, \" 3")); - List groupsForFive = new ArrayList<>(); - groupsForFive.add(db.getGroup("group, \" 1")); - groupsForFive.add(db.getGroup("group, \" 3")); + List groupsForFive = new ArrayList<>(); + groupsForFive.add(db.getGroup("group, \" 1")); + groupsForFive.add(db.getGroup("group, \" 3")); - db.setLoyaltyCardGroups(1, groupsForOne); - db.setLoyaltyCardGroups(2, groupsForTwo); - db.setLoyaltyCardGroups(3, groupsForThree); - db.setLoyaltyCardGroups(4, groupsForFour); - db.setLoyaltyCardGroups(5, groupsForFive); + db.setLoyaltyCardGroups(1, groupsForOne); + db.setLoyaltyCardGroups(2, groupsForTwo); + db.setLoyaltyCardGroups(3, groupsForThree); + db.setLoyaltyCardGroups(4, groupsForFour); + db.setLoyaltyCardGroups(5, groupsForFive); - ByteArrayOutputStream outData = new ByteArrayOutputStream(); - OutputStreamWriter outStream = new OutputStreamWriter(outData); + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); - // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, format); - assertTrue(result); - outStream.close(); + // Export data to CSV format + boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + assertTrue(result); + outStream.close(); - clearDatabase(); + clearDatabase(); - ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); - // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); - assertTrue(result); + // Import the CSV data + result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + assertTrue(result); - assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); - assertEquals(NUM_GROUPS, db.getGroupCount()); + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + assertEquals(NUM_GROUPS, db.getGroupCount()); - checkLoyaltyCards(); - checkGroups(); + checkLoyaltyCards(); + checkGroups(); - assertEquals(groupsToGroupNames(groupsForOne), groupsToGroupNames(db.getLoyaltyCardGroups(1))); - assertEquals(groupsToGroupNames(groupsForTwo), groupsToGroupNames(db.getLoyaltyCardGroups(2))); - assertEquals(groupsToGroupNames(groupsForThree), groupsToGroupNames(db.getLoyaltyCardGroups(3))); - assertEquals(groupsToGroupNames(groupsForFour), groupsToGroupNames(db.getLoyaltyCardGroups(4))); - assertEquals(groupsToGroupNames(groupsForFive), groupsToGroupNames(db.getLoyaltyCardGroups(5))); - assertEquals(emptyGroup, db.getLoyaltyCardGroups(6)); - assertEquals(emptyGroup, db.getLoyaltyCardGroups(7)); - assertEquals(emptyGroup, db.getLoyaltyCardGroups(8)); - assertEquals(emptyGroup, db.getLoyaltyCardGroups(9)); - assertEquals(emptyGroup, db.getLoyaltyCardGroups(10)); + assertEquals(groupsToGroupNames(groupsForOne), groupsToGroupNames(db.getLoyaltyCardGroups(1))); + assertEquals(groupsToGroupNames(groupsForTwo), groupsToGroupNames(db.getLoyaltyCardGroups(2))); + assertEquals(groupsToGroupNames(groupsForThree), groupsToGroupNames(db.getLoyaltyCardGroups(3))); + assertEquals(groupsToGroupNames(groupsForFour), groupsToGroupNames(db.getLoyaltyCardGroups(4))); + assertEquals(groupsToGroupNames(groupsForFive), groupsToGroupNames(db.getLoyaltyCardGroups(5))); + assertEquals(emptyGroup, db.getLoyaltyCardGroups(6)); + assertEquals(emptyGroup, db.getLoyaltyCardGroups(7)); + assertEquals(emptyGroup, db.getLoyaltyCardGroups(8)); + assertEquals(emptyGroup, db.getLoyaltyCardGroups(9)); + assertEquals(emptyGroup, db.getLoyaltyCardGroups(10)); - // Clear the database for the next format under test - clearDatabase(); - } + // Clear the database for the next format under test + clearDatabase(); } @Test @@ -482,32 +472,29 @@ public class ImportExportTest { final int NUM_CARDS = 10; - for(DataFormat format : DataFormat.values()) - { - addLoyaltyCards(NUM_CARDS); + addLoyaltyCards(NUM_CARDS); - ByteArrayOutputStream outData = new ByteArrayOutputStream(); - OutputStreamWriter outStream = new OutputStreamWriter(outData); + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); - // Export into CSV data - boolean result = MultiFormatExporter.exportData(db, outStream, format); - assertTrue(result); - outStream.close(); + // Export into CSV data + boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); + assertTrue(result); + outStream.close(); - ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); - // Import the CSV data on top of the existing database - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); - assertTrue(result); + // Import the CSV data on top of the existing database + result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + assertTrue(result); - assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); - checkLoyaltyCards(); + checkLoyaltyCards(); - // Clear the database for the next format under test - clearDatabase(); - } + // Clear the database for the next format under test + clearDatabase(); } @Test @@ -523,7 +510,7 @@ public class ImportExportTest OutputStreamWriter outStream = new OutputStreamWriter(outData); // Export data to CSV format - boolean result = MultiFormatExporter.exportData(db, outStream, format); + boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima); assertTrue(result); clearDatabase(); @@ -537,8 +524,8 @@ public class ImportExportTest ByteArrayInputStream inData = new ByteArrayInputStream((outData.toString() + corruptEntry).getBytes()); InputStreamReader inStream = new InputStreamReader(inData); - // Attempt to import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + // Attempt to import the data + result = MultiFormatImporter.importData(db, inStream, format); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -566,49 +553,46 @@ public class ImportExportTest final File sdcardDir = Environment.getExternalStorageDirectory(); final File exportFile = new File(sdcardDir, "Catima.csv"); - for(DataFormat format : DataFormat.values()) - { - addLoyaltyCards(NUM_CARDS); + addLoyaltyCards(NUM_CARDS); - TestTaskCompleteListener listener = new TestTaskCompleteListener(); + TestTaskCompleteListener listener = new TestTaskCompleteListener(); - // Export to the file - FileOutputStream fileOutputStream = new FileOutputStream(exportFile); - ImportExportTask task = new ImportExportTask(activity, format, fileOutputStream, listener); - task.execute(); + // Export to the file + FileOutputStream fileOutputStream = new FileOutputStream(exportFile); + ImportExportTask task = new ImportExportTask(activity, DataFormat.Catima, fileOutputStream, listener); + task.execute(); - // Actually run the task to completion - Robolectric.flushBackgroundThreadScheduler(); + // Actually run the task to completion + Robolectric.flushBackgroundThreadScheduler(); - // Check that the listener was executed - assertNotNull(listener.success); - assertEquals(true, listener.success); + // Check that the listener was executed + assertNotNull(listener.success); + assertEquals(true, listener.success); - clearDatabase(); + clearDatabase(); - // Import everything back from the default location + // Import everything back from the default location - listener = new TestTaskCompleteListener(); + listener = new TestTaskCompleteListener(); - FileInputStream fileStream = new FileInputStream(exportFile); + FileInputStream fileStream = new FileInputStream(exportFile); - task = new ImportExportTask(activity, format, fileStream, listener); - task.execute(); + task = new ImportExportTask(activity, DataFormat.Catima, fileStream, listener); + task.execute(); - // Actually run the task to completion - Robolectric.flushBackgroundThreadScheduler(); + // Actually run the task to completion + Robolectric.flushBackgroundThreadScheduler(); - // Check that the listener was executed - assertNotNull(listener.success); - assertEquals(true, listener.success); + // Check that the listener was executed + assertNotNull(listener.success); + assertEquals(true, listener.success); - assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); - checkLoyaltyCards(); + checkLoyaltyCards(); - // Clear the database for the next format under test - clearDatabase(); - } + // Clear the database for the next format under test + clearDatabase(); } @Test @@ -628,7 +612,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -666,7 +650,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -704,7 +688,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -730,7 +714,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -768,7 +752,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -806,7 +790,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -844,7 +828,7 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -864,7 +848,7 @@ public class ImportExportTest inStream = new InputStreamReader(inputStream); // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -912,7 +896,8 @@ public class ImportExportTest InputStreamReader inStream = new InputStreamReader(inputStream); // Import the Voucher Vault data - new VoucherVaultImporter().importData(db, inStream); + boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.VoucherVault); + assertTrue(result); assertEquals(2, db.getLoyaltyCardCount()); LoyaltyCard card = db.getLoyaltyCard(1); From c13b5dda1811d57076e4825853583b51f9bb5c87 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 4 Mar 2021 23:45:03 +0100 Subject: [PATCH 29/77] Make Spotbugs happy --- .../protect/card_locker/importexport/MultiFormatExporter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index f6509d561..fedc446e7 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -33,6 +33,9 @@ public class MultiFormatExporter case Catima: exporter = new CsvDatabaseExporter(); break; + default: + Log.e(TAG, "Failed to export data, unknown format " + format.name()); + break; } if(exporter != null) From 0ce71039f1d6d45bd234ff0e2502d3c3138ec63d Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 5 Mar 2021 23:44:27 +0100 Subject: [PATCH 30/77] Split into screen on and lockscreen bypass --- CHANGELOG.md | 2 ++ .../protect/card_locker/LoyaltyCardViewActivity.java | 10 ++++++---- .../java/protect/card_locker/preferences/Settings.java | 7 ++++++- app/src/main/res/values/strings.xml | 4 +++- app/src/main/res/xml/preferences.xml | 8 +++++++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56951fb39..17cbc03d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Changes: - Support importing [Voucher Vault](https://github.com/tim-smart/vouchervault/) exports +- Option to keep the screen on while viewing a loyalty card +- Option to suspend the lock screen while viewing a loyalty card ## v1.9.2 (2021-02-24) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 9c8943506..6ae355190 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -245,10 +245,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity attributes.screenBrightness = 1F; } - if (settings.getKeepScreenOn()) - { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON| - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD| + if (settings.getKeepScreenOn()) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + if (settings.getDisableLockscreenWhileViewingCard()) { + window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); } diff --git a/app/src/main/java/protect/card_locker/preferences/Settings.java b/app/src/main/java/protect/card_locker/preferences/Settings.java index 7cb0aa715..ab5bc96cb 100644 --- a/app/src/main/java/protect/card_locker/preferences/Settings.java +++ b/app/src/main/java/protect/card_locker/preferences/Settings.java @@ -93,6 +93,11 @@ public class Settings public boolean getKeepScreenOn() { - return getBoolean(R.string.settings_key_keep_screen_on, false); + return getBoolean(R.string.settings_key_keep_screen_on, true); + } + + public boolean getDisableLockscreenWhileViewingCard() + { + return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0ffec37c..3181ac81d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,8 +104,10 @@ pref_display_card_max_brightness Lock barcode orientation pref_lock_barcode_orientation - Keep screen on + Keep screen on while viewing a card pref_keep_screen_on + Disable lock screen while viewing a card + pref_disable_lockscreen_while_viewing_card sharedpreference_active_tab diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 654949865..e8b14331d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -60,10 +60,16 @@ app:iconSpaceReserved="false" /> + + \ No newline at end of file From 6d1e7ee3bb44405463ad248da80629da1d87d0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 5 Mar 2021 09:15:34 +0000 Subject: [PATCH 31/77] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.4% (127 of 129 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index ccdbd22e9..803bf2b6e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -119,4 +119,5 @@ Saldo %s poeng Saldo: %s - + Hvilket program vil du importere data fra\? + \ No newline at end of file From 9bd71d5694d57e546e3268d2981d225ea34255c7 Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Fri, 5 Mar 2021 08:51:49 +0000 Subject: [PATCH 32/77] Translated using Weblate (Dutch) Currently translated at 100.0% (129 of 129 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0e6158c74..e8020488f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -119,4 +119,5 @@ Saldo %s punten Saldo: %s - + Uit welke app wil je gegevens importeren\? + \ No newline at end of file From ba9d1f389145748b68c66e8a1a756182b5803950 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Fri, 5 Mar 2021 23:56:38 +0100 Subject: [PATCH 33/77] Make Loyalty Card Keychain app name translatable Because, well, the app itself uses different names in different countries --- CHANGELOG.md | 2 +- app/src/main/res/values/arrays.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cbc03d9..99b952c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Changes: - Support importing [Voucher Vault](https://github.com/tim-smart/vouchervault/) exports - Option to keep the screen on while viewing a loyalty card -- Option to suspend the lock screen while viewing a loyalty card +- Option to suspend the lock screen while viewing a loyalty card ## v1.9.2 (2021-02-24) diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 303cc27bf..dcc72889c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2,7 +2,7 @@ Catima - Loyalty Card Keychain + @string/app_loyalty_card_keychain Voucher Vault \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3181ac81d..af194e9dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,4 +156,5 @@ %s does not seem to be a valid balance. Import data from which app? + Loyalty Card Keychain From 15116ab673515c15f8c4f5edf75cc7f3a307511a Mon Sep 17 00:00:00 2001 From: BMN Date: Sat, 6 Mar 2021 17:02:51 +0000 Subject: [PATCH 34/77] Translated using Weblate (Italian) Currently translated at 100.0% (134 of 134 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/ --- app/src/main/res/values-it/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e854bfad5..0a39897af 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -119,4 +119,8 @@ Saldo: %s Scaduta: %s Scade: %s - + Mantieni schermo acceso durante la visualizzazione di una carta + Loyalty Card Keychain + Da quale app vuoi importare i dati\? + Mantieni schermo attivo mentre visualizzi una carta + \ No newline at end of file From bfd36bae9fe3d57bbffb5d3b80906e117183e0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 6 Mar 2021 08:59:23 +0000 Subject: [PATCH 35/77] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.5% (132 of 134 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 803bf2b6e..06450f79e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -120,4 +120,7 @@ %s poeng Saldo: %s Hvilket program vil du importere data fra\? + Kundekortsknippe + Skru av låseskjerm under kortvisning + Behold skjerm påslått under kortvisning \ No newline at end of file From f9a66ba3a0b8ea38fc9fce16b6974ac6abc95ece Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Sat, 6 Mar 2021 10:54:47 +0000 Subject: [PATCH 36/77] Translated using Weblate (Dutch) Currently translated at 100.0% (134 of 134 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e8020488f..2f695022d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -120,4 +120,7 @@ %s punten Saldo: %s Uit welke app wil je gegevens importeren\? + Klantenkaartkluis + Vergrendelscherm uitschakelen tijdens tonen van kaart + Scherm niet uitschakelen tijdens tonen van kaart \ No newline at end of file From 810fa5f621e249dba9554e238b39982126cb8848 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 7 Mar 2021 14:38:38 +0100 Subject: [PATCH 37/77] Release 1.10 --- CHANGELOG.md | 2 +- app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b952c74..76c970747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## v1.10 (2021-03-07) Changes: diff --git a/app/build.gradle b/app/build.gradle index a7d9df8c6..10d25f9c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 62 - versionName "1.9.2" + versionCode 63 + versionName "1.10" vectorDrawables.useSupportLibrary true } From 237405279feaebe2c5bf4c1e2ec3fa74a7e12281 Mon Sep 17 00:00:00 2001 From: Schi Ri Date: Thu, 11 Mar 2021 00:06:25 +0000 Subject: [PATCH 38/77] Translated using Weblate (German) Currently translated at 100.0% (134 of 134 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/ --- app/src/main/res/values-de/strings.xml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 171706f69..ec0ee83df 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -103,4 +103,24 @@ %d Karten Gruppen: %s - + Loyalty Card Keychain + Aus welcher App Daten importieren\? + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> scheint kein gültiges Guthaben zu sein. + Punkte + Währung + Guthaben + Barcode auf dem Bildschirm zentrieren + Barcode auf dem Bildschirm nach oben schieben + Ablaufdatum wählen + Nie + Ablaufdatum + Barcode bearbeiten + Barcode + Karte + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> Punkte + Guthaben: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Abgelaufen: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Läuft ab: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Sperrbildschirm deaktivieren während eine Karte angesehen wird + Bildschirm anlassen während eine Karte angesehen wird + \ No newline at end of file From d1bdab5c6645fc465037dbaf7ee67e92b96d0a9b Mon Sep 17 00:00:00 2001 From: Schi Ri Date: Thu, 11 Mar 2021 00:00:53 +0000 Subject: [PATCH 39/77] Translated using Weblate (German) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/ --- fastlane/metadata/android/de-DE/short_description.txt | 2 +- fastlane/metadata/android/de-DE/title.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index f62e7d002..f6f663eec 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Verwaltet barcode-basierte Kunden-/Treuekarten auf dem Handy \ No newline at end of file +Verwaltet barcode-basierte Kunden-/Treuekarten auf dem Handy diff --git a/fastlane/metadata/android/de-DE/title.txt b/fastlane/metadata/android/de-DE/title.txt index 23c018cfe..84c21ff5c 100644 --- a/fastlane/metadata/android/de-DE/title.txt +++ b/fastlane/metadata/android/de-DE/title.txt @@ -1 +1 @@ -Catima +Catima - Kundenkarten- & Ticket-Manager From 70b313021cc5c08e7682eb93bd764a911591ada9 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sat, 13 Mar 2021 17:05:20 +0100 Subject: [PATCH 40/77] Fix incorrect escaping from Weblate --- app/src/main/res/values-de/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ec0ee83df..1e25956a8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -105,7 +105,7 @@ Gruppen: %s Loyalty Card Keychain Aus welcher App Daten importieren\? - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> scheint kein gültiges Guthaben zu sein. + %s scheint kein gültiges Guthaben zu sein. Punkte Währung Guthaben @@ -117,10 +117,10 @@ Barcode bearbeiten Barcode Karte - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> Punkte - Guthaben: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Abgelaufen: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Läuft ab: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + %s Punkte + Guthaben: %s + Abgelaufen: %s + Läuft ab: %s Sperrbildschirm deaktivieren während eine Karte angesehen wird Bildschirm anlassen während eine Karte angesehen wird - \ No newline at end of file + From 9b4085e95588d212bc7165e877ebb0ec182b835f Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 14 Mar 2021 17:58:20 +0100 Subject: [PATCH 41/77] Show privacy policy dialog on first start --- CHANGELOG.md | 6 +++ .../protect/card_locker/MainActivity.java | 40 +++++++++++++++++++ app/src/main/res/menu/main_menu.xml | 4 ++ app/src/main/res/values/strings.xml | 5 +++ docs/privacy-policy.md | 2 +- 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c970747..e98c8de40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +Changes: + +- Add privacy policy dialog on first start (required by Huawei) + ## v1.10 (2021-03-07) Changes: diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 1faf8f23f..7e5712ab2 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -1,13 +1,16 @@ package protect.card_locker; +import android.app.AlertDialog; import android.app.SearchManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; @@ -92,6 +95,29 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O helpText.setOnTouchListener(gestureTouchListener); noMatchingCardsText.setOnTouchListener(gestureTouchListener); list.setOnTouchListener(gestureTouchListener); + + // Show privacy policy on first run + SharedPreferences privacyPolicyShownPref = getApplicationContext().getSharedPreferences( + getString(R.string.sharedpreference_privacy_policy_shown), + Context.MODE_PRIVATE); + + if (privacyPolicyShownPref.getInt(getString(R.string.sharedpreference_privacy_policy_shown), 0) == 0) { + SharedPreferences.Editor privacyPolicyShownPrefEditor = privacyPolicyShownPref.edit(); + privacyPolicyShownPrefEditor.putInt(getString(R.string.sharedpreference_privacy_policy_shown), 1); + privacyPolicyShownPrefEditor.apply(); + + new AlertDialog.Builder(this) + .setTitle(R.string.privacy_policy) + .setMessage(R.string.privacy_policy_popup_text) + .setPositiveButton(R.string.thank_you, null) + .setNegativeButton(R.string.privacy_policy, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + openPrivacyPolicy(); + } + }) + .setIcon(android.R.drawable.ic_dialog_info) + .show(); + }; } @Override @@ -290,6 +316,14 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O groupsTabLayout.setVisibility(View.VISIBLE); } + private void openPrivacyPolicy() { + Intent browserIntent = new Intent( + Intent.ACTION_VIEW, + Uri.parse("https://thelastproject.github.io/Catima/privacy-policy") + ); + startActivity(browserIntent); + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { @@ -402,6 +436,12 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O return true; } + if(id == R.id.action_privacy_policy) + { + openPrivacyPolicy(); + return true; + } + if(id == R.id.action_about) { Intent i = new Intent(getApplicationContext(), AboutActivity.class); diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index d02adb230..1384f9852 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -22,6 +22,10 @@ android:id="@+id/action_settings" android:title="@string/settings" app:showAsAction="never"/> + pref_disable_lockscreen_while_viewing_card sharedpreference_active_tab + sharedpreference_privacy_policy_shown I want to share a card with you thelastproject.github.io @@ -157,4 +158,8 @@ %s does not seem to be a valid balance. Import data from which app? Loyalty Card Keychain + + Privacy Policy + Thank you + Many app stores require apps to show their privacy policy on first start. Here is ours:\n\nWe collect NO DATA AT ALL and our app is Open Source so anyone can confirm this to be true. diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md index 55fc66ca2..c4bcfcc24 100644 --- a/docs/privacy-policy.md +++ b/docs/privacy-policy.md @@ -1,4 +1,4 @@ -# PRIVACY POLICY FOR LOYALTY CARD KEYCHAIN +# PRIVACY POLICY FOR CATIMA This privacy policy governs your use of the software application Catima (“Application”) for mobile devices that was created by Sylvia van Os. The Application is designed to store and display barcodes. From dcc5e5921ce292c8427962e88b427a585d45dd0f Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Sun, 14 Mar 2021 21:34:51 +0100 Subject: [PATCH 42/77] Fix unit tests --- app/src/test/java/protect/card_locker/MainActivityTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/protect/card_locker/MainActivityTest.java b/app/src/test/java/protect/card_locker/MainActivityTest.java index 34c35c06e..a849fd7bf 100644 --- a/app/src/test/java/protect/card_locker/MainActivityTest.java +++ b/app/src/test/java/protect/card_locker/MainActivityTest.java @@ -61,10 +61,11 @@ public class MainActivityTest assertTrue(menu != null); // The settings, import/export, groups, search and add button should be present - assertEquals(menu.size(), 5); + assertEquals(menu.size(), 6); assertEquals("Search", menu.findItem(R.id.action_search).getTitle().toString()); assertEquals("Groups", menu.findItem(R.id.action_manage_groups).getTitle().toString()); assertEquals("Import/Export", menu.findItem(R.id.action_import_export).getTitle().toString()); + assertEquals("Privacy Policy", menu.findItem(R.id.action_privacy_policy).getTitle().toString()); assertEquals("About", menu.findItem(R.id.action_about).getTitle().toString()); assertEquals("Settings", menu.findItem(R.id.action_settings).getTitle().toString()); } From aee59550e4643b7603dea1c2c0a112cf25388550 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 15 Mar 2021 12:32:56 +0000 Subject: [PATCH 43/77] Translated using Weblate (Russian) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 55 +++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bca032d91..ab7eff21a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5,7 +5,7 @@ Нажмите кнопку + (плюс), чтобы сначала добавить карту. \n \nCatima хранит ваши карты на устройстве, поэтому они всегда под рукой. - "Ничего не найдено. Попробуйте изменить свой поиск." + Ничего не найдено. Попробуйте изменить поисковый запрос. Магазин Примечание Номер карты @@ -20,7 +20,7 @@ Блокировать поворот экрана Автоповорот экрана Удаление карты - Пожалуйста подтвердите удаление карты. + Подтвердите удаление карты. ОК Скопировать номер карты в буфер обмена Переслать @@ -43,7 +43,7 @@ Не удалось импортировать карты Экспортировано Экспорт не удался - Не удалось экспортировать + Невозможно экспортировать карты Импорт… Экспорт… Импорт или экспорт невозможен без разрешения на доступ к хранилищу @@ -51,18 +51,18 @@ Импорт из файловой системы Выберете файл на файловой системе. Выбрать файл - Использование другого приложения + Использовать другое приложение Используйте любое приложение или ваш любимый файловый менеджер, чтобы открыть файл. Использовать другое приложение О программе Авторское лево свободного программного обеспечения, лицензия GPLv3+. - О программе %s + О приложении <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_name\">%s</xliff:g> Версия: %s - Информация о версиях: %s + Информация о версии: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_revision_url\">%s</xliff:g> %s использует следующие сторонние библиотеки: %s %s использует следующие сторонние ресурсы: %s Выбор штрих-кода - Введите ID карты и выберите тип штрих-кода. + Введите номер карты и выберите тип штрих-кода. Номер карты скопирован в буфер обмена Логотип карты Настройки @@ -71,9 +71,9 @@ Системная Светлая Тёмная - Размер шрифта для названий карт - Размер шрифта примечания для списка - Размер шрифта названия карты + Размер шрифта названий карт (режим списка) + Размер шрифта примечания (режим списка) + Размер шрифта названия карты (просмотр) Размер шрифта номера карты Максимальная яркость при показе карты Портретная ориентация экрана при показе карты @@ -95,4 +95,39 @@ Вы уверены, что хотите покинуть этот экран\? Изменения не будут сохранены. Выйти без сохранения Не удалось открыть файловый менеджер. Пожалуйста, убедитись, что он установлен. + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> баллов + Баланс: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Истёк срок действия: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Истекает срок действия: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Баллы + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> не похож на правильный баланс. + Ручной ввод номера карты + Многие магазины приложений требуют, чтобы приложение показывало свою политику конфиденциальности при первом запуске. Вот наша: +\n +\nМы НЕ собираем НИКАКИХ ДАННЫХ и наше приложение имеет открытый исходный код, так что любой может убедиться в том, что это правда. + Спасибо + Политика конфиденциальности + Loyalty Card Keychain + Из какого приложения импортировать данные\? + Баланс + Переместить штрих-код в верхнюю часть экрана + Центрировать штрих-код на экране + Валюта + Введите дату окончания срока действия + Никогда + Дата окончания срока действия + Изменить штрих-код + Штрих-код + Карта + Группы: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Переместить ниже в вписке + Переместить выше в вписке + Отключать блокировку экрана во время отображения карты + Держать экран включённым во время отображения карты + + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карта + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карты + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карт + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карт + \ No newline at end of file From a9794daca5d9b5aa54b048955e44cf01f1805376 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 15 Mar 2021 12:53:52 +0000 Subject: [PATCH 44/77] Translated using Weblate (Russian) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ab7eff21a..97d2e1818 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,20 +1,20 @@ Поиск - Добавить карту - Нажмите кнопку + (плюс), чтобы сначала добавить карту. + Добавить + Нажмите кнопку + (плюс), чтобы добавить карту. \n \nCatima хранит ваши карты на устройстве, поэтому они всегда под рукой. Ничего не найдено. Попробуйте изменить поисковый запрос. Магазин Примечание Номер карты - Тип штрихкода - Эта карта без штрихкода + Тип штрих-кода + Эта карта без штрих-кода Отменить Сохранить - Редактировать штрих-код - Редактировать + Изменить штрих-код + Изменить Удалить карту Подтвердить Блокировать поворот экрана @@ -34,15 +34,15 @@ Название магазина не указано Номер карты не указан Карта не найдена - Не удалось разобрать импортируемый URI - Импорт/Экспорт + Невозможно разобрать импортируемый URI + Импорт/экспорт Экспорт Резервное копирование карт позволяет переместить их на другое устройство. Импортировано - Импорт не удался - Не удалось импортировать карты + Импорт не выполнен + Невозможно импортировать карты Экспортировано - Экспорт не удался + Экспорт не выполнен Невозможно экспортировать карты Импорт… Экспорт… @@ -54,7 +54,7 @@ Использовать другое приложение Используйте любое приложение или ваш любимый файловый менеджер, чтобы открыть файл. Использовать другое приложение - О программе + О приложении Авторское лево свободного программного обеспечения, лицензия GPLv3+. О приложении <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_name\">%s</xliff:g> Версия: %s @@ -71,22 +71,22 @@ Системная Светлая Тёмная - Размер шрифта названий карт (режим списка) + Размер шрифта названия карты (режим списка) Размер шрифта примечания (режим списка) Размер шрифта названия карты (просмотр) Размер шрифта номера карты - Максимальная яркость при показе карты - Портретная ориентация экрана при показе карты + Максимальная яркость при отображении карты + Портретная ориентация экрана при отображении карты Я хочу поделиться картой с вами Данные карты экспортированы Все - Нажмите кнопку + (плюс), чтобы сначала добавить группы. + Нажмите кнопку + (плюс), чтобы добавить группы. \n \nГруппы упрощают поиск. Группы Введите название группы Данные карты лояльности успешно импортированы - Любимая звезда + Звезда избранного На основе Loyalty Card Keychain, авторские права 2016–2020 Branden Archer. Удалить из избранного Добавить в избранное @@ -94,7 +94,7 @@ Подтвердите, что хотите удалить эту группу Вы уверены, что хотите покинуть этот экран\? Изменения не будут сохранены. Выйти без сохранения - Не удалось открыть файловый менеджер. Пожалуйста, убедитись, что он установлен. + Невозможно открыть файловый менеджер. Убедитесь, что он установлен. <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> баллов Баланс: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> Истёк срок действия: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> @@ -113,7 +113,7 @@ Переместить штрих-код в верхнюю часть экрана Центрировать штрих-код на экране Валюта - Введите дату окончания срока действия + Укажите дату окончания срока действия Никогда Дата окончания срока действия Изменить штрих-код @@ -122,8 +122,8 @@ Группы: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> Переместить ниже в вписке Переместить выше в вписке - Отключать блокировку экрана во время отображения карты - Держать экран включённым во время отображения карты + Отключать блокировку экрана при отображении карты + Держать экран включённым при отображении карты <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карта <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карты From 5a6b7944b1b1b5536030e4a988aedd27d0776eb7 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 15 Mar 2021 13:09:22 +0000 Subject: [PATCH 45/77] Translated using Weblate (Russian) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 97d2e1818..49925253b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -25,7 +25,7 @@ Скопировать номер карты в буфер обмена Переслать Отправить… - Редактировать карту + Изменить карту Добавить карту Отсканируйте штрих-код Ярлык карты @@ -71,8 +71,8 @@ Системная Светлая Тёмная - Размер шрифта названия карты (режим списка) - Размер шрифта примечания (режим списка) + Размер шрифта названия карты (список) + Размер шрифта примечания (список) Размер шрифта названия карты (просмотр) Размер шрифта номера карты Максимальная яркость при отображении карты @@ -85,7 +85,7 @@ \nГруппы упрощают поиск. Группы Введите название группы - Данные карты лояльности успешно импортированы + Данные карты успешно импортированы Звезда избранного На основе Loyalty Card Keychain, авторские права 2016–2020 Branden Archer. Удалить из избранного @@ -113,9 +113,9 @@ Переместить штрих-код в верхнюю часть экрана Центрировать штрих-код на экране Валюта - Укажите дату окончания срока действия + Указать срок действия Никогда - Дата окончания срока действия + Окончание срока действия Изменить штрих-код Штрих-код Карта From bc5252b2efe38f5ce79b1d156d09810c4d2a1185 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 15 Mar 2021 19:33:44 +0100 Subject: [PATCH 46/77] Update incorrect Weblate escaping --- app/src/main/res/values-ru/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 49925253b..62a89b019 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -56,9 +56,9 @@ Использовать другое приложение О приложении Авторское лево свободного программного обеспечения, лицензия GPLv3+. - О приложении <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_name\">%s</xliff:g> + О приложении %s Версия: %s - Информация о версии: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_revision_url\">%s</xliff:g> + Информация о версии: %s %s использует следующие сторонние библиотеки: %s %s использует следующие сторонние ресурсы: %s Выбор штрих-кода @@ -95,12 +95,12 @@ Вы уверены, что хотите покинуть этот экран\? Изменения не будут сохранены. Выйти без сохранения Невозможно открыть файловый менеджер. Убедитесь, что он установлен. - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> баллов - Баланс: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Истёк срок действия: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> - Истекает срок действия: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + %s баллов + Баланс: %s + Истёк срок действия: %s + Истекает срок действия: %s Баллы - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> не похож на правильный баланс. + %s не похож на правильный баланс. Ручной ввод номера карты Многие магазины приложений требуют, чтобы приложение показывало свою политику конфиденциальности при первом запуске. Вот наша: \n @@ -119,15 +119,15 @@ Изменить штрих-код Штрих-код Карта - Группы: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Группы: %s Переместить ниже в вписке Переместить выше в вписке Отключать блокировку экрана при отображении карты Держать экран включённым при отображении карты - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карта - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карты - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карт - <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%d</xliff:g> карт + %d карта + %d карты + %d карт + %d карт - \ No newline at end of file + From 97553e9253e64ea6598cab35dc7d7378f40965c1 Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Sun, 14 Mar 2021 17:18:32 +0000 Subject: [PATCH 47/77] Translated using Weblate (Dutch) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2f695022d..f8dd70c6e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -123,4 +123,9 @@ Klantenkaartkluis Vergrendelscherm uitschakelen tijdens tonen van kaart Scherm niet uitschakelen tijdens tonen van kaart + Veel appwinkels vereisen dat apps na de eerste keer opstarten hun privacybeleid tonen. Dat van ons is eenvoudig: +\n +\nWe verzamelen HELEMAAL NIKS. Bovendien is onze app open source, zodat een ieder met eigen ogen kan zien wat de app wel of niet doet. + Bedankt + Privacybeleid \ No newline at end of file From d221969b5efa1ec12c1274fb4c427cc19c7e2ba8 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 15 Mar 2021 13:25:04 +0000 Subject: [PATCH 48/77] Translated using Weblate (Russian) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 49925253b..9dcbfadf1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -51,14 +51,14 @@ Импорт из файловой системы Выберете файл на файловой системе. Выбрать файл - Использовать другое приложение + Использование другого приложения Используйте любое приложение или ваш любимый файловый менеджер, чтобы открыть файл. Использовать другое приложение О приложении Авторское лево свободного программного обеспечения, лицензия GPLv3+. О приложении <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_name\">%s</xliff:g> Версия: %s - Информация о версии: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_revision_url\">%s</xliff:g> + Информация о версиях: <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_revision_url\">%s</xliff:g> %s использует следующие сторонние библиотеки: %s %s использует следующие сторонние ресурсы: %s Выбор штрих-кода From 353d8a7ecd940498eaf8d75ea69f34a5bbf3214c Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 15 Mar 2021 19:37:45 +0100 Subject: [PATCH 49/77] Fix typo --- app/src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 62a89b019..2730b7384 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -58,7 +58,7 @@ Авторское лево свободного программного обеспечения, лицензия GPLv3+. О приложении %s Версия: %s - Информация о версии: %s + Информация о версии: %s %s использует следующие сторонние библиотеки: %s %s использует следующие сторонние ресурсы: %s Выбор штрих-кода From f51ad0295ae722efe36cf9a5706647a4077edb82 Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 16 Mar 2021 14:08:53 +0000 Subject: [PATCH 50/77] Translated using Weblate (Russian) Currently translated at 100.0% (138 of 138 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6c8dcbdab..0de38deae 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -75,8 +75,8 @@ Размер шрифта примечания (список) Размер шрифта названия карты (просмотр) Размер шрифта номера карты - Максимальная яркость при отображении карты - Портретная ориентация экрана при отображении карты + Максимальная яркость при показе карты + Портретная ориентация экрана при показе карты Я хочу поделиться картой с вами Данные карты экспортированы Все @@ -122,12 +122,12 @@ Группы: %s Переместить ниже в вписке Переместить выше в вписке - Отключать блокировку экрана при отображении карты - Держать экран включённым при отображении карты + Не блокировать экран при показе карты + Не отключать экран при показе карты %d карта %d карты %d карт %d карт - + \ No newline at end of file From ef61aaeac6aed9e3cbef67490b50ec97dd653280 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 20 Mar 2021 05:10:43 +0000 Subject: [PATCH 51/77] Translated using Weblate (Russian) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/ --- fastlane/metadata/android/ru-RU/full_description.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 000000000..05743275f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,7 @@ +Надоело искать свою пластиковую скидочную карту на кассе в магазине? Ищете бесплатное решение, которое не будет собирать вашу личную информацию? + +Catima — это приложение, которое будет хранить карты лояльности на основе штрих-кодов на вашем смартфоне. Это приложение с открытым исходным кодом и пытается сделать хорошо только одну вещь : управлять вашими картами! + +Новые карты можно добавить в одно мгновение. Либо используйте камеру, чтобы считать штрих-код, либо введите номер карты вручную. Добавленный в приложение штрих-код можно считать с экрана смартфона в магазине с современным сканером штрих-кода. (В некоторых магазинах вместо сканеров изображений используются старые сканеры штрих-кодов, например, планшетные сканеры. Они не могут считывать данные с экрана. Вместо этого попросите сотрудника ввести номер вручную.). + +Приложению требуется очень мало разрешений и не нужен доступ в интернет. Есть возможность резервного копирования ваших карт в локальное хранилище. Оттуда вы сможете перенести данные резервной копии в надёжное место. From eba1ed63a6e229aabe48265acebdc5ab28dd61bb Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 23 Mar 2021 09:35:43 +0100 Subject: [PATCH 52/77] Release 1.11 --- CHANGELOG.md | 2 +- app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98c8de40..b652d9dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## v1.11 (2021-03-21) Changes: diff --git a/app/build.gradle b/app/build.gradle index 10d25f9c4..a8aff6174 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 19 targetSdkVersion 29 - versionCode 63 - versionName "1.10" + versionCode 64 + versionName "1.11" vectorDrawables.useSupportLibrary true } From bd1d33867df9b4d23d98ea45161c99937186ce6c Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 23 Mar 2021 21:01:56 +0100 Subject: [PATCH 53/77] Comply with Huawei's pedantic nonsense https://twitter.com/SylvieLorxu/status/1374251557735256065 --- CHANGELOG.md | 6 ++++++ app/src/main/java/protect/card_locker/MainActivity.java | 2 +- app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el-rGR/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-he-rIL/strings.xml | 3 +-- app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 2 -- app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 2 -- app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values/strings.xml | 3 +-- 16 files changed, 9 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b652d9dbb..189e3fc02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +Changes: + +- Change "Thank you" text on privacy dialog to "Accept" because Huawei is overly pedantic + ## v1.11 (2021-03-21) Changes: diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 7e5712ab2..1440a3fb8 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -109,7 +109,7 @@ public class MainActivity extends AppCompatActivity implements GestureDetector.O new AlertDialog.Builder(this) .setTitle(R.string.privacy_policy) .setMessage(R.string.privacy_policy_popup_text) - .setPositiveButton(R.string.thank_you, null) + .setPositiveButton(R.string.accept, null) .setNegativeButton(R.string.privacy_policy, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { openPrivacyPolicy(); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1e25956a8..23d833912 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -11,7 +11,6 @@ Kartennummer Abbrechen Speichern - Karte bearbeiten Bearbeiten Löschen Bestätigen diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index bfd7dded7..f9d8843b9 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -7,7 +7,6 @@ Κωδικός Κάρτας Άκυρο Αποθήκευση - Επεξεργασία Κάρτας Επεξεργασία Διαγραφή Επιβεβαίωση diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d1c9a4992..9c5fd5422 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,6 @@ Id. de tarjeta Cancelar Guardar - Editar tarjeta Editar Eliminar Confirmar diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a222e1ec6..11f79b58a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,7 +9,6 @@ Numéro Annuler Enregistrer - Modifier Modifier Supprimer Confirmer diff --git a/app/src/main/res/values-he-rIL/strings.xml b/app/src/main/res/values-he-rIL/strings.xml index d19047bba..b04df4902 100644 --- a/app/src/main/res/values-he-rIL/strings.xml +++ b/app/src/main/res/values-he-rIL/strings.xml @@ -6,5 +6,4 @@ מזהה כרטיס ביטול שמור - עריכת כרטיס - + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0a39897af..af1fea448 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -12,7 +12,6 @@ Questa carta non ha un codice a barre Annulla Salva - Modifica carta Modifica Elimina Conferma diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c576ff10e..00f739d84 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -44,7 +44,6 @@ 확인 삭제 편집 - 카드 편집 저장 취소 즐겨찾기에서 제거 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 06450f79e..d6743fe0f 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -10,7 +10,6 @@ Strekkodetype Avbryt Lagre - Rediger kort Rediger Slett Bekreft diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f8dd70c6e..34fb2d90d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -13,7 +13,6 @@ Deze kaart heeft geen barcode Annuleren Opslaan - Kaart bewerken Bewerken Verwijderen Bevestigen @@ -126,6 +125,5 @@ Veel appwinkels vereisen dat apps na de eerste keer opstarten hun privacybeleid tonen. Dat van ons is eenvoudig: \n \nWe verzamelen HELEMAAL NIKS. Bovendien is onze app open source, zodat een ieder met eigen ogen kan zien wat de app wel of niet doet. - Bedankt Privacybeleid \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3ea0ca6bd..c5d3ba292 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -10,7 +10,6 @@ Ta karta nie ma kodu kreskowego Anuluj Zapisz - Edytuj kartę Edytuj Usuń Potwierdź diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0de38deae..360eba2ed 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -13,7 +13,6 @@ Эта карта без штрих-кода Отменить Сохранить - Изменить штрих-код Изменить Удалить карту Подтвердить @@ -105,7 +104,6 @@ Многие магазины приложений требуют, чтобы приложение показывало свою политику конфиденциальности при первом запуске. Вот наша: \n \nМы НЕ собираем НИКАКИХ ДАННЫХ и наше приложение имеет открытый исходный код, так что любой может убедиться в том, что это правда. - Спасибо Политика конфиденциальности Loyalty Card Keychain Из какого приложения импортировать данные\? diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 7fbbce650..d675d748f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -9,7 +9,6 @@ ID karty Zrušiť Uložiť - Úprava karty Upraviť Vymazať Potvrdiť diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 51f45c030..74ca7d549 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -9,7 +9,6 @@ Št. kartice Prekliči Shrani - Uredi kartico Uredi Izbriši Potrdi diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3acad1531..1a1bee696 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,6 @@ Cancel Save - Edit Card Edit Delete Confirm @@ -160,6 +159,6 @@ Loyalty Card Keychain Privacy Policy - Thank you Many app stores require apps to show their privacy policy on first start. Here is ours:\n\nWe collect NO DATA AT ALL and our app is Open Source so anyone can confirm this to be true. + Accept From b9e152e3c4308178f6e2e933f72c2dd9f41d93e7 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 24 Mar 2021 20:35:58 +0100 Subject: [PATCH 54/77] Add Fidme import support --- CHANGELOG.md | 1 + .../java/protect/card_locker/DBHelper.java | 21 +++ .../java/protect/card_locker/DataFormat.java | 1 + .../card_locker/ImportExportActivity.java | 35 ++++- .../protect/card_locker/ImportExportTask.java | 12 +- .../card_locker/importexport/CSVHelpers.java | 93 +++++++++++ .../importexport/CsvDatabaseImporter.java | 122 +++------------ .../importexport/DatabaseImporter.java | 4 +- .../importexport/FidmeImporter.java | 144 ++++++++++++++++++ .../importexport/MultiFormatImporter.java | 6 +- .../importexport/VoucherVaultImporter.java | 5 +- app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 8 + 13 files changed, 335 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/importexport/CSVHelpers.java create mode 100644 app/src/main/java/protect/card_locker/importexport/FidmeImporter.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 189e3fc02..c230f73d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changes: +- Support importing [Fidme](https://play.google.com/store/apps/details?id=fr.snapp.fidme) exports - Change "Thank you" text on privacy dialog to "Accept" because Huawei is overly pedantic ## v1.11 (2021-03-21) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index ce27a76cf..c81cbcddf 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -166,6 +166,27 @@ public class DBHelper extends SQLiteOpenHelper return newId; } + public boolean insertLoyaltyCard(final SQLiteDatabase db, final String store, + final String note, final Date expiry, final BigDecimal balance, + final Currency balanceType, final String cardId, + final String barcodeType, final Integer headerColor, + final int starStatus) + { + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbIds.STORE, store); + contentValues.put(LoyaltyCardDbIds.NOTE, note); + contentValues.put(LoyaltyCardDbIds.EXPIRY, expiry != null ? expiry.getTime() : null); + contentValues.put(LoyaltyCardDbIds.BALANCE, balance.toString()); + contentValues.put(LoyaltyCardDbIds.BALANCE_TYPE, balanceType != null ? balanceType.getCurrencyCode() : null); + contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); + contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); + contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor); + contentValues.put(LoyaltyCardDbIds.HEADER_TEXT_COLOR, Color.WHITE); + contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus); + final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues); + return (newId != -1); + } + public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store, final String note, final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId, diff --git a/app/src/main/java/protect/card_locker/DataFormat.java b/app/src/main/java/protect/card_locker/DataFormat.java index 4b4b23e08..c030653a7 100644 --- a/app/src/main/java/protect/card_locker/DataFormat.java +++ b/app/src/main/java/protect/card_locker/DataFormat.java @@ -3,6 +3,7 @@ package protect.card_locker; public enum DataFormat { Catima, + Fidme, VoucherVault ; } diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index cb70b396f..97b243b75 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -16,6 +16,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import android.provider.ContactsContract; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -28,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; +import java.text.DateFormat; public class ImportExportActivity extends AppCompatActivity { @@ -39,6 +41,8 @@ public class ImportExportActivity extends AppCompatActivity private ImportExportTask importExporter; + private String importAlertTitle; + private String importAlertMessage; private DataFormat importDataFormat; @Override @@ -120,18 +124,43 @@ public class ImportExportActivity extends AppCompatActivity switch (which) { // Catima case 0: - // Loyalty Card Keychain + importAlertTitle = getString(R.string.importCatima); + importAlertMessage = getString(R.string.importCatimaMessage); + importDataFormat = DataFormat.Catima; + break; + // Fidme case 1: + importAlertTitle = getString(R.string.importFidme); + importAlertMessage = getString(R.string.importFidmeMessage); + importDataFormat = DataFormat.Fidme; + break; + // Loyalty Card Keychain + case 2: + importAlertTitle = getString(R.string.importLoyaltyCardKeychain); + importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage); importDataFormat = DataFormat.Catima; break; // Voucher Vault - case 2: + case 3: + importAlertTitle = getString(R.string.importVoucherVault); + importAlertMessage = getString(R.string.importVoucherVaultMessage); importDataFormat = DataFormat.VoucherVault; break; default: throw new IllegalArgumentException("Unknown DataFormat"); } - chooseFileWithIntent(baseIntent, IMPORT); + + new AlertDialog.Builder(this) + .setTitle(importAlertTitle) + .setMessage(importAlertMessage) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + chooseFileWithIntent(baseIntent, IMPORT); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); }); builder.show(); } diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index 402d57181..9ea99c627 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -61,16 +61,8 @@ class ImportExportTask extends AsyncTask { boolean result = false; - try - { - InputStreamReader reader = new InputStreamReader(stream, Charset.forName("UTF-8")); - result = MultiFormatImporter.importData(db, reader, format); - reader.close(); - } - catch(IOException e) - { - Log.e(TAG, "Unable to import file", e); - } + + result = MultiFormatImporter.importData(db, stream, format); Log.i(TAG, "Import result: " + result); diff --git a/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java new file mode 100644 index 000000000..74dd8f94e --- /dev/null +++ b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java @@ -0,0 +1,93 @@ +package protect.card_locker.importexport; + +import org.apache.commons.csv.CSVRecord; + +import protect.card_locker.FormatException; + +public class CSVHelpers { + /** + * Extract a string from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, defaultValue is returned + * if it is not null. Otherwise, a FormatException is thrown. + */ + static String extractString(String key, CSVRecord record, String defaultValue) + throws FormatException + { + String toReturn = defaultValue; + + if(record.isMapped(key)) + { + toReturn = record.get(key); + } + else + { + if(defaultValue == null) + { + throw new FormatException("Field not used but expected: " + key); + } + } + + return toReturn; + } + + /** + * Extract an integer from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, or the data is not a valid + * int, a FormatException is thrown. + */ + static Integer extractInt(String key, CSVRecord record, boolean nullIsOk) + throws FormatException + { + if(record.isMapped(key) == false) + { + throw new FormatException("Field not used but expected: " + key); + } + + String value = record.get(key); + if(value.isEmpty() && nullIsOk) + { + return null; + } + + try + { + return Integer.parseInt(record.get(key)); + } + catch(NumberFormatException e) + { + throw new FormatException("Failed to parse field: " + key, e); + } + } + + /** + * Extract a long from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, or the data is not a valid + * int, a FormatException is thrown. + */ + static Long extractLong(String key, CSVRecord record, boolean nullIsOk) + throws FormatException + { + if(record.isMapped(key) == false) + { + throw new FormatException("Field not used but expected: " + key); + } + + String value = record.get(key); + if(value.isEmpty() && nullIsOk) + { + return null; + } + + try + { + return Long.parseLong(record.get(key)); + } + catch(NumberFormatException e) + { + throw new FormatException("Failed to parse field: " + key, e); + } + } +} diff --git a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java index c2491bc42..7654fd3d5 100644 --- a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java @@ -5,15 +5,21 @@ import android.database.sqlite.SQLiteDatabase; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; +import org.json.JSONException; import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; +import java.text.ParseException; import java.util.Currency; import java.util.Date; import java.util.List; +import java.util.zip.ZipFile; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; @@ -28,9 +34,9 @@ import protect.card_locker.Group; */ public class CsvDatabaseImporter implements DatabaseImporter { - public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException + public void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException { - BufferedReader bufferedReader = new BufferedReader(input); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input)); bufferedReader.mark(100); @@ -200,92 +206,6 @@ public class CsvDatabaseImporter implements DatabaseImporter } } - /** - * Extract a string from the items array. The index into the array - * is determined by looking up the index in the fields map using the - * "key" as the key. If no such key exists, defaultValue is returned - * if it is not null. Otherwise, a FormatException is thrown. - */ - private String extractString(String key, CSVRecord record, String defaultValue) - throws FormatException - { - String toReturn = defaultValue; - - if(record.isMapped(key)) - { - toReturn = record.get(key); - } - else - { - if(defaultValue == null) - { - throw new FormatException("Field not used but expected: " + key); - } - } - - return toReturn; - } - - /** - * Extract an integer from the items array. The index into the array - * is determined by looking up the index in the fields map using the - * "key" as the key. If no such key exists, or the data is not a valid - * int, a FormatException is thrown. - */ - private Integer extractInt(String key, CSVRecord record, boolean nullIsOk) - throws FormatException - { - if(record.isMapped(key) == false) - { - throw new FormatException("Field not used but expected: " + key); - } - - String value = record.get(key); - if(value.isEmpty() && nullIsOk) - { - return null; - } - - try - { - return Integer.parseInt(record.get(key)); - } - catch(NumberFormatException e) - { - throw new FormatException("Failed to parse field: " + key, e); - } - } - - /** - * Extract a long from the items array. The index into the array - * is determined by looking up the index in the fields map using the - * "key" as the key. If no such key exists, or the data is not a valid - * int, a FormatException is thrown. - */ - private Long extractLong(String key, CSVRecord record, boolean nullIsOk) - throws FormatException - { - if(record.isMapped(key) == false) - { - throw new FormatException("Field not used but expected: " + key); - } - - String value = record.get(key); - if(value.isEmpty() && nullIsOk) - { - return null; - } - - try - { - return Long.parseLong(record.get(key)); - } - catch(NumberFormatException e) - { - throw new FormatException("Failed to parse field: " + key, e); - } - } - /** * Import a single loyalty card into the database using the given * session. @@ -293,23 +213,23 @@ public class CsvDatabaseImporter implements DatabaseImporter private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record) throws IOException, FormatException { - int id = extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false); + int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false); - String store = extractString(DBHelper.LoyaltyCardDbIds.STORE, record, ""); + String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, ""); if(store.isEmpty()) { throw new FormatException("No store listed, but is required"); } - String note = extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, ""); + String note = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, ""); Date expiry = null; try { - expiry = new Date(extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true)); + expiry = new Date(CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true)); } catch (NullPointerException | FormatException e) { } BigDecimal balance; try { - balance = new BigDecimal(extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null)); + balance = new BigDecimal(CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null)); } catch (FormatException _e ) { // These fields did not exist in versions 1.8.1 and before // We catch this exception so we can still import old backups @@ -317,29 +237,29 @@ public class CsvDatabaseImporter implements DatabaseImporter } Currency balanceType = null; - String unparsedBalanceType = extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, ""); + String unparsedBalanceType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, ""); if(!unparsedBalanceType.isEmpty()) { balanceType = Currency.getInstance(unparsedBalanceType); } - String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); + String cardId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); if(cardId.isEmpty()) { throw new FormatException("No card ID listed, but is required"); } - String barcodeType = extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, ""); + String barcodeType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, ""); Integer headerColor = null; if(record.isMapped(DBHelper.LoyaltyCardDbIds.HEADER_COLOR)) { - headerColor = extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true); + headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true); } int starStatus = 0; try { - starStatus = extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false); + starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false); } catch (FormatException _e ) { // This field did not exist in versions 0.28 and before // We catch this exception so we can still import old backups @@ -355,7 +275,7 @@ public class CsvDatabaseImporter implements DatabaseImporter private void importGroup(SQLiteDatabase database, DBHelper helper, CSVRecord record) throws IOException, FormatException { - String id = extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null); + String id = CSVHelpers.extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null); helper.insertGroup(database, id); } @@ -367,8 +287,8 @@ public class CsvDatabaseImporter implements DatabaseImporter private void importCardGroupMapping(SQLiteDatabase database, DBHelper helper, CSVRecord record) throws IOException, FormatException { - Integer cardId = extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false); - String groupId = extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null); + Integer cardId = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false); + String groupId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null); List cardGroups = helper.getLoyaltyCardGroups(cardId); cardGroups.add(helper.getGroup(groupId)); diff --git a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java index 867454385..afb320eda 100644 --- a/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/DatabaseImporter.java @@ -2,7 +2,9 @@ package protect.card_locker.importexport; import org.json.JSONException; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.text.ParseException; @@ -21,5 +23,5 @@ public interface DatabaseImporter * @throws IOException * @throws FormatException */ - void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException, JSONException, ParseException; + void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException; } diff --git a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java new file mode 100644 index 000000000..d1979d12a --- /dev/null +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -0,0 +1,144 @@ +package protect.card_locker.importexport; + +import android.annotation.SuppressLint; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; + +import com.google.zxing.BarcodeFormat; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Currency; +import java.util.Date; +import java.util.TimeZone; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import protect.card_locker.DBHelper; +import protect.card_locker.FormatException; + +/** + * Class for importing a database from CSV (Comma Separate Values) + * formatted data. + * + * The database's loyalty cards are expected to appear in the CSV data. + * A header is expected for the each table showing the names of the columns. + */ +public class FidmeImporter implements DatabaseImporter +{ + public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { + // We actually retrieve a .zip file + ZipInputStream zipInputStream = new ZipInputStream(input); + + StringBuilder loyaltyCards = new StringBuilder(); + byte[] buffer = new byte[1024]; + int read = 0; + + ZipEntry zipEntry; + + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (zipEntry.getName().equals("loyalty_programs.csv")) { + while ((read = zipInputStream.read(buffer, 0, 1024)) >= 0) { + loyaltyCards.append(new String(buffer, 0, read)); + } + } + } + + if (loyaltyCards.length() == 0) { + throw new FormatException("Couldn't find loyalty_programs.csv in zip file or it is empty"); + } + + SQLiteDatabase database = db.getWritableDatabase(); + database.beginTransaction(); + + final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.withDelimiter(';').withHeader()); + + try { + for (CSVRecord record : fidmeParser) { + importLoyaltyCard(database, db, record); + + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + } + } catch (IllegalArgumentException | IllegalStateException | InterruptedException e) { + throw new FormatException("Issue parsing CSV data", e); + } finally { + fidmeParser.close(); + } + + database.setTransactionSuccessful(); + database.endTransaction(); + database.close(); + } + + /** + * Import a single loyalty card into the database using the given + * session. + */ + private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record) + throws IOException, FormatException + { + // A loyalty card export from Fidme contains the following fields: + // Retailer (store name) + // Program (program name) + // Added at (YYYY-MM-DD HH:MM:SS UTC) + // Reference (card ID) + // Firstname (card holder first name) + // Lastname (card holder last name) + + // The store is called Retailer + String store = CSVHelpers.extractString("Retailer", record, ""); + + if (store.isEmpty()) + { + throw new FormatException("No store listed, but is required"); + } + + // There seems to be no note field in the CSV? So let's combine other fields instead... + String program = CSVHelpers.extractString("Program", record, ""); + String addedAt = CSVHelpers.extractString("Added At", record, ""); + String firstName = CSVHelpers.extractString("Firstname", record, ""); + String lastName = CSVHelpers.extractString("Lastname", record, ""); + + String combinedName = String.format("%s %s", firstName, lastName); + + StringBuilder noteBuilder = new StringBuilder(); + if (!program.isEmpty()) noteBuilder.append(program).append('\n'); + if (!addedAt.isEmpty()) noteBuilder.append(addedAt).append('\n'); + if (!combinedName.isEmpty()) noteBuilder.append(combinedName).append('\n'); + String note = noteBuilder.toString(); + + // The ID is called reference + String cardId = CSVHelpers.extractString("Reference", record, ""); + if(cardId.isEmpty()) + { + throw new FormatException("No card ID listed, but is required"); + } + + // Sadly, Fidme exports don't contain the card type + // I guess they have an online DB of all the different companies and what type they use + // TODO: Hook this into our own loyalty card DB if we ever get one + String barcodeType = ""; + + // No favourite data in the export either + int starStatus = 0; + + helper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, barcodeType, null, starStatus); + } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 3186440cd..4d5324521 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -5,6 +5,7 @@ import android.util.Log; import org.json.JSONException; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.text.ParseException; @@ -29,7 +30,7 @@ public class MultiFormatImporter * false otherwise. If false, no data was written to * the database. */ - public static boolean importData(DBHelper db, InputStreamReader input, DataFormat format) + public static boolean importData(DBHelper db, InputStream input, DataFormat format) { DatabaseImporter importer = null; @@ -38,6 +39,9 @@ public class MultiFormatImporter case Catima: importer = new CsvDatabaseImporter(); break; + case Fidme: + importer = new FidmeImporter(); + break; case VoucherVault: importer = new VoucherVaultImporter(); break; 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 317adb65a..8c1121a6d 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -12,6 +12,7 @@ import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigDecimal; import java.text.ParseException; @@ -32,8 +33,8 @@ import protect.card_locker.FormatException; */ public class VoucherVaultImporter implements DatabaseImporter { - public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, JSONException, ParseException { - BufferedReader bufferedReader = new BufferedReader(input); + public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input)); StringBuilder sb = new StringBuilder(); String line; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index dcc72889c..504bd052d 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2,6 +2,7 @@ Catima + Fidme @string/app_loyalty_card_keychain Voucher Vault diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a1bee696..94d916534 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,4 +161,12 @@ Privacy Policy Many app stores require apps to show their privacy policy on first start. Here is ours:\n\nWe collect NO DATA AT ALL and our app is Open Source so anyone can confirm this to be true. Accept + Import from Catima + Please select your Catima export file. It is most likely named Catima.csv.\n\nA Catima export file can be created by going to the Import/Export menu and pressing "Export". + Import from Fidme + Please select your Fidme export file. It is most likely named something like fidme-export-request-xxxxxx.zip.\n\nA Fidme export file can be created in the Fidme app by going to your profile, choosing "Data Protection" and then pressing "Extract my data".\n\nPlease note that Fidme does not store the barcode type in the export data so you will have to edit the imported cards manually. + Import from Loyalty Card Keychain + Please select your Loyalty Card Keychain export file. It is most likely named LoyaltyCardKeychain.csv.\n\nA Loyalty Card Keychain export file can be created by going to the Import/Export menu and pressing "Export". + Import from Voucher Vault + Please select your Voucher Vault export file. It is most likely named vouchervault.json.\n\nA Voucher Vault export file can be created by pressing "Export". From afb960257ef24abaf8b5109c5f1860d5a2e208fa Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 22:58:12 +0000 Subject: [PATCH 55/77] Translated using Weblate (German) Currently translated at 100.0% (137 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/ --- app/src/main/res/values-de/strings.xml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 23d833912..103784282 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -102,19 +102,19 @@ %d Karten Gruppen: %s - Loyalty Card Keychain + Kundenkartenanhänger Aus welcher App Daten importieren\? %s scheint kein gültiges Guthaben zu sein. Punkte Währung Guthaben - Barcode auf dem Bildschirm zentrieren - Barcode auf dem Bildschirm nach oben schieben + Strichcode auf dem Bildschirm zentrieren + Strichcode auf dem Bildschirm nach oben schieben Ablaufdatum wählen Nie Ablaufdatum - Barcode bearbeiten - Barcode + Strichcode bearbeiten + Strichcode Karte %s Punkte Guthaben: %s @@ -122,4 +122,9 @@ Läuft ab: %s Sperrbildschirm deaktivieren während eine Karte angesehen wird Bildschirm anlassen während eine Karte angesehen wird - + Es wird oft verlangt, dass Anwendungen beim ersten Start ihre Datenschutzrichtlinien anzeigen. Hier ist unsere: +\n +\nWir sammeln KEINE DATEN und unsere Anwendung ist quelloffen, sodass jeder bestätigen kann, dass dies wahr ist. + Annehmen + Datenschutzrichtlinie + \ No newline at end of file From 8fbbfb137ec17c2d0e55cc248f19aed592120a0d Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:21:07 +0000 Subject: [PATCH 56/77] Translated using Weblate (Greek) Currently translated at 44.5% (61 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/el/ --- app/src/main/res/values-el-rGR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index f9d8843b9..798f091d6 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -63,4 +63,5 @@ Σκοτεινό Φωτεινό Σύστημα + Γραμμικός κώδικας \ No newline at end of file From f62352cf0517461ac68db7bfe5ba89e3df40d53e Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:20:33 +0000 Subject: [PATCH 57/77] Translated using Weblate (Spanish (Argentina)) Currently translated at 14.5% (20 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/es_AR/ --- app/src/main/res/values-es-rAR/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index c8d3a6422..2de21eb26 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -1,4 +1,4 @@ - - - + + + Código de barras + \ No newline at end of file From 7636c3648cf1bfcc94bc225c6acde29c53f6047d Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 22:57:05 +0000 Subject: [PATCH 58/77] Translated using Weblate (French) Currently translated at 100.0% (137 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/ --- app/src/main/res/values-fr/strings.xml | 29 ++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 11f79b58a..7213a3ca6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -4,7 +4,7 @@ Appuyez d\'abord sur le bouton \"+\" (plus) pour ajouter une carte. \n \nCatima enregistre vos cartes sur votre appareil, pour toujours les avoir à portée de main. - Nom du Magasin + Magasin Notes Numéro Annuler @@ -102,4 +102,29 @@ %d cartes Groupes : %s - + Accepter + De nombreux magasins d\'applications exigent que les applications affichent leur politique de confidentialité au premier démarrage. Voici la nôtre : +\n +\nNous ne collectons AUCUNE DONNÉE et le code source de notre application est ouvert afin que tout le monde puisse le confirmer. + Politique de confidentialité + Porte-clé de carte de fidélité + Importer des données à partir de quelle application \? + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> ne semble pas être un solde valide. + Points + Monnaie + Solde + Centrer le code-barres sur l\'écran + Déplacez le code-barres vers le haut de l\'écran + Choisissez la date d\'expiration + Jamais + Date d\'expiration + Modifier le code-barres + Code-barres + Carte + <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> points + Solde : <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Expiré : <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Expire : <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> + Désactiver l\'écran de verrouillage pendant l\'affichage d\'une carte + Garder l\'écran allumé pendant l\'affichage d\'une carte + \ No newline at end of file From 0b92a126949414aed9b2b46ea3a6ae221df3b2e8 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:09:21 +0000 Subject: [PATCH 59/77] Translated using Weblate (Italian) Currently translated at 100.0% (137 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/ --- app/src/main/res/values-it/strings.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index af1fea448..8d06d5911 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -119,7 +119,12 @@ Scaduta: %s Scade: %s Mantieni schermo acceso durante la visualizzazione di una carta - Loyalty Card Keychain + Portachiavi carta fedeltà Da quale app vuoi importare i dati\? Mantieni schermo attivo mentre visualizzi una carta + Accetta + È spesso richiesto alle applicazioni di mostrare la loro politica sulla riservatezza al primo avvio. Ecco la nostra: +\n +\nNon raccogliamo ALCUN DATO e il codice sorgente della nostra applicazione è aperto, quindi chiunque può confermare che ciò sia vero. + Informativa sulla riservatezza \ No newline at end of file From cf871e960682361b18b01a12531ccc7fe318b963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Tue, 23 Mar 2021 23:16:25 +0000 Subject: [PATCH 60/77] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 97.8% (134 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index d6743fe0f..77cf28b46 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -122,4 +122,9 @@ Kundekortsknippe Skru av låseskjerm under kortvisning Behold skjerm påslått under kortvisning + Mange programbutikker krever at programmer viser personvernspraksisen sin ved første oppstart. Her er vår: +\n +\nVi SAMLER IKKE IN NOEN DATA overhodet, og programmet vårt er fri programvare, så alle kan bekrefte at dette stemmer. + Godta + Personvernspraksis \ No newline at end of file From bb1eae6f79b1694bdc9c1efb42fe7316a335351f Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Tue, 23 Mar 2021 20:08:32 +0000 Subject: [PATCH 61/77] Translated using Weblate (Dutch) Currently translated at 100.0% (137 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 34fb2d90d..fd0124cae 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -126,4 +126,5 @@ \n \nWe verzamelen HELEMAAL NIKS. Bovendien is onze app open source, zodat een ieder met eigen ogen kan zien wat de app wel of niet doet. Privacybeleid + Accepteren \ No newline at end of file From 7dadce600b2e0a7f62764247e97def2be09547f3 Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 23 Mar 2021 22:19:11 +0000 Subject: [PATCH 62/77] Translated using Weblate (Russian) Currently translated at 100.0% (137 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 360eba2ed..ca6b48e74 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -128,4 +128,5 @@ %d карт %d карт + Принять \ No newline at end of file From 1213ee99dabdb1ccd93390097f25cd7f1d1e84aa Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:08:52 +0000 Subject: [PATCH 63/77] Translated using Weblate (French) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/ --- fastlane/metadata/android/fr-FR/title.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/fr-FR/title.txt b/fastlane/metadata/android/fr-FR/title.txt index fd0df72e6..a92975488 100644 --- a/fastlane/metadata/android/fr-FR/title.txt +++ b/fastlane/metadata/android/fr-FR/title.txt @@ -1 +1 @@ -Catima – Cartes de fidélité +Catima – Gestionnaire de cartes de fidélité From e937fc60a724b1acfdf37bdef6d875d70c47c019 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:04:59 +0000 Subject: [PATCH 64/77] Translated using Weblate (German) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/ --- fastlane/metadata/android/de-DE/full_description.txt | 8 ++++---- fastlane/metadata/android/de-DE/short_description.txt | 2 +- fastlane/metadata/android/de-DE/title.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index cd6d29abf..b4a80e4e5 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,7 +1,7 @@ -Bist Du es auch leid, beim Bezahlen an der Kasse im Supermarkt jedes mal nach dieser Bonuskarte aus Plastik zu suchen? Genervt davon, dass die Brieftasche durch die ganzen Plastikkarten gefühlte fünf Meter dick ist? Wäre es nicht toll, eine freie Lösung für das Problem zu haben, die Deine Daten nicht noch zusätzlich mit weiteren Anbietern teilt? +Bist du es auch leid, beim Bezahlen an der Kasse im Supermarkt jedes mal nach dieser Bonuskarte aus Plastik zu suchen? Genervt davon, dass die Brieftasche durch die ganzen Plastikkarten gefühlte fünf Meter dick ist? Wäre es nicht toll, eine freie Lösung für das Problem zu haben, die deine Daten nicht noch zusätzlich mit weiteren Anbietern teilt? -Catima ist eine App, die Deine auf Barcodes basierenden Kundenkarten auf Deinem Smartphone verwaltet. Catima ist Open Source und kann eines richtig gut: Deine Karten verwalten! +Catima ist eine Anwendung, die deine auf Strichcodes basierenden Kundenkarten auf deinem Smartphone verwaltet. Catima ist quelloffen und kann eines richtig gut: Deine Karten verwalten! -Neue Karten können mit Leichtigkeit hinzugefügt werden. Entweder nutzt Du die Kamera Deines Smartphones, um den Barcode direkt einzulesen – oder Du gibst die unter selbigem befindliche Ziffernfolge von Hand ein. An der Kasse zeigt Catima sodann den Barcode auf dem Display Deines Smartphones an, sodass er vom Scanner gelesen werden kann. Sollte der Scanner dabei doch einmal streiken (was selten vorkommt – und wenn, dann meist nur bei älteren Scanner-Modellen), kann der Verkäufer die ebenfalls angezeigten Ziffern wiederum auch händisch erfassen. +Neue Karten können mit Leichtigkeit hinzugefügt werden. Entweder nutzt du die Kamera deines Smartphones, um den Strichcode direkt einzulesen – oder du gibst die unter selbigem befindliche Ziffernfolge von Hand ein. An der Kasse zeigt Catima sodann den Strichcode auf dem Bildschirm deines Mobiltelefones an, sodass er vom Scanner gelesen werden kann. Sollte der Scanner dabei doch einmal streiken (was selten vorkommt – und wenn, dann meist nur bei älteren Scanner-Modellen), kann der Verkäufer die ebenfalls angezeigten Ziffern wiederum auch händisch erfassen. -Die App benötigt nur wenige Berechtigungen – und greift nie auf das Internet zu. Catima bietet auch die Möglichkeit, Deine Kartensammlung zu exportieren sowie zu importieren. Damit kannst Du die Daten z. B. auch auf ein anderes Gerät übertragen – oder etwa nach einem Werksreset wieder neu einlesen. +Die Anwendung benötigt nur wenige Berechtigungen – und greift nie auf das Internet zu. Catima bietet auch die Möglichkeit, deine Kartensammlung zu exportieren sowie zu importieren. Damit kannst du die Daten z. B. auch auf ein anderes Gerät übertragen – oder etwa nach einem Werksreset wieder neu einlesen. diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index f6f663eec..ee35cd54a 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Verwaltet barcode-basierte Kunden-/Treuekarten auf dem Handy +Verwaltet Strichcode-basierte Kunden-/Treuekarten auf dem Mobiltelefon diff --git a/fastlane/metadata/android/de-DE/title.txt b/fastlane/metadata/android/de-DE/title.txt index 84c21ff5c..71cca707c 100644 --- a/fastlane/metadata/android/de-DE/title.txt +++ b/fastlane/metadata/android/de-DE/title.txt @@ -1 +1 @@ -Catima - Kundenkarten- & Ticket-Manager +Catima - Kundenkarten- und Ticketverwaltung From a1bb6e3bed0530109b6d75baded75741f5e63cb2 Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Tue, 23 Mar 2021 20:09:06 +0000 Subject: [PATCH 65/77] Translated using Weblate (Dutch) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/nl/ --- fastlane/metadata/android/nl-NL/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt index cfc0bbdfc..be63de060 100644 --- a/fastlane/metadata/android/nl-NL/full_description.txt +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -2,6 +2,6 @@ Ben jij het ook zo zat om tijdens het afrekenen steeds weer te moeten graaien na Dan is Catima wat voor jou! Met deze app kun je je op barcode gebaseerde klantenkaarten opslaan op je telefoon. De app is open source en heeft maar één doel: het beheren en organiseren van je kaarten! -Je voegt nieuwe kaarten in een handomdraai toe, met je camera of door het nummer ervan in te voeren. In de winkel hoef je vervolgens alleen maar de kaart te openen zodat deze kan worden gescand door elke moderne barcodescanner. (Sommige winkels gebruiken nog ouderwetse barcodescanners, zoals flatbedscanners, welke je scherm niet kunnen aflezen. In dat geval moet de kassamedewerker het nummer handmatig invoeren.). +Je voegt nieuwe kaarten in een handomdraai toe, met je camera of door het nummer ervan in te voeren. In de winkel hoef je vervolgens alleen maar de kaart te openen zodat deze kan worden gescand door elke moderne barcodescanner. (Sommige winkels gebruiken nog ouderwetse barcodescanners, zoals scanners, welke je scherm niet kunnen aflezen. In dat geval moet de kassamedewerker het nummer handmatig invoeren.). Catima vereist nauwelijks rechten en maakt geen contact met het internet. Je kunt je kaarten back-uppen naar je lokale opslag en de back-up vervolgens overzetten naar een veilig opslagmedium. From d2de5db792e6a22a36d04e2be945b6a961306878 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:20:47 +0000 Subject: [PATCH 66/77] Translated using Weblate (Korean) Currently translated at 73.7% (101 of 137 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ko/ --- app/src/main/res/values-ko/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 00f739d84..4e5da5960 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -83,4 +83,5 @@ 매장을 입력하지 않음 즐겨찾기 별 바코드를 표시할 때 화면 밝기 높이기 + 바코드 \ No newline at end of file From 6e63543cd1d3215e78ed6684370cfe36612bf2fb Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 23 Mar 2021 23:08:33 +0000 Subject: [PATCH 67/77] Translated using Weblate (Italian) Currently translated at 66.6% (2 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/ --- fastlane/metadata/android/it/short_description.txt | 1 + fastlane/metadata/android/it/title.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/it/short_description.txt create mode 100644 fastlane/metadata/android/it/title.txt diff --git a/fastlane/metadata/android/it/short_description.txt b/fastlane/metadata/android/it/short_description.txt new file mode 100644 index 000000000..ff51269c3 --- /dev/null +++ b/fastlane/metadata/android/it/short_description.txt @@ -0,0 +1 @@ +Gestisce le carte fedeltà basate su codici a barre sul telefono diff --git a/fastlane/metadata/android/it/title.txt b/fastlane/metadata/android/it/title.txt new file mode 100644 index 000000000..10b2d1f4c --- /dev/null +++ b/fastlane/metadata/android/it/title.txt @@ -0,0 +1 @@ +Catima – Gestore di carte fedeltà From 30971b7e85faf5aecc6105b479e5d3459fee49c3 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 24 Mar 2021 21:17:02 +0100 Subject: [PATCH 68/77] Fix unit tests --- .../protect/card_locker/ImportExportTest.java | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 11413b6b8..645583942 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -334,10 +334,9 @@ public class ImportExportTest clearDatabase(); ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -366,10 +365,9 @@ public class ImportExportTest clearDatabase(); ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -440,10 +438,9 @@ public class ImportExportTest clearDatabase(); ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -483,10 +480,9 @@ public class ImportExportTest outStream.close(); ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); - InputStreamReader inStream = new InputStreamReader(inData); // Import the CSV data on top of the existing database - result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + result = MultiFormatImporter.importData(db, inData, DataFormat.Catima); assertTrue(result); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); @@ -522,10 +518,9 @@ public class ImportExportTest String corruptEntry = "ThisStringIsLikelyNotPartOfAnyFormat,\"\"a"; ByteArrayInputStream inData = new ByteArrayInputStream((outData.toString() + corruptEntry).getBytes()); - InputStreamReader inStream = new InputStreamReader(inData); // Attempt to import the data - result = MultiFormatImporter.importData(db, inStream, format); + result = MultiFormatImporter.importData(db, inData, format); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -609,10 +604,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,0"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -647,10 +641,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,,,0"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -685,10 +678,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,not a number,invalid,0"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertEquals(false, result); assertEquals(0, db.getLoyaltyCardCount()); @@ -711,10 +703,9 @@ public class ImportExportTest csvText += "1,store,note,12345,,1,1,0"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -749,10 +740,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,1,1,1"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -787,10 +777,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,1,1,"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertEquals(true, result); assertEquals(1, db.getLoyaltyCardCount()); @@ -825,10 +814,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,1,1,2"; ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the CSV data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -845,10 +833,9 @@ public class ImportExportTest csvText += "1,store,note,12345,type,1,1,text"; inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8)); - inStream = new InputStreamReader(inputStream); // Import the CSV data - result = MultiFormatImporter.importData(db, inStream, DataFormat.Catima); + result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima); assertTrue(result); assertEquals(1, db.getLoyaltyCardCount()); @@ -893,10 +880,9 @@ public class ImportExportTest "]"; ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonText.getBytes(StandardCharsets.UTF_8)); - InputStreamReader inStream = new InputStreamReader(inputStream); // Import the Voucher Vault data - boolean result = MultiFormatImporter.importData(db, inStream, DataFormat.VoucherVault); + boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.VoucherVault); assertTrue(result); assertEquals(2, db.getLoyaltyCardCount()); From f59f9ddec8a2b9be9ee97287d0ddc70f311e146d Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Wed, 24 Mar 2021 21:33:36 +0100 Subject: [PATCH 69/77] Make spotBugs happy --- .../card_locker/importexport/CsvDatabaseImporter.java | 4 +++- .../java/protect/card_locker/importexport/FidmeImporter.java | 5 ++++- .../card_locker/importexport/VoucherVaultImporter.java | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java index 7654fd3d5..2054521c7 100644 --- a/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CsvDatabaseImporter.java @@ -15,6 +15,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.Currency; import java.util.Date; @@ -36,7 +38,7 @@ public class CsvDatabaseImporter implements DatabaseImporter { public void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException { - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input)); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); bufferedReader.mark(100); 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 d1979d12a..1f57cf1fd 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Currency; @@ -54,7 +55,7 @@ public class FidmeImporter implements DatabaseImporter while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (zipEntry.getName().equals("loyalty_programs.csv")) { while ((read = zipInputStream.read(buffer, 0, 1024)) >= 0) { - loyaltyCards.append(new String(buffer, 0, read)); + loyaltyCards.append(new String(buffer, 0, read, StandardCharsets.UTF_8)); } } } @@ -85,6 +86,8 @@ public class FidmeImporter implements DatabaseImporter database.setTransactionSuccessful(); database.endTransaction(); database.close(); + + zipInputStream.close(); } /** 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 8c1121a6d..30c6bcb66 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Currency; @@ -34,7 +35,7 @@ import protect.card_locker.FormatException; public class VoucherVaultImporter implements DatabaseImporter { public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException { - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input)); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); String line; From a4c24c6436c2542d77cc6bf555951f672e61d24d Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Thu, 25 Mar 2021 00:00:44 +0100 Subject: [PATCH 70/77] Fix multiline note cutoff --- CHANGELOG.md | 1 + .../res/layout/loyalty_card_view_layout.xml | 25 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c230f73d6..a83b45718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changes: - Support importing [Fidme](https://play.google.com/store/apps/details?id=fr.snapp.fidme) exports +- Fix multiline note cutoff - Change "Thank you" text on privacy dialog to "Accept" because Huawei is overly pedantic ## v1.11 (2021-03-21) diff --git a/app/src/main/res/layout/loyalty_card_view_layout.xml b/app/src/main/res/layout/loyalty_card_view_layout.xml index 4a78483cc..db4c0124f 100644 --- a/app/src/main/res/layout/loyalty_card_view_layout.xml +++ b/app/src/main/res/layout/loyalty_card_view_layout.xml @@ -135,6 +135,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:padding="20dp" android:visibility="gone" app:behavior_hideable="false" app:behavior_peekHeight="104dp" @@ -155,46 +156,38 @@ + android:textSize="@dimen/singleCardNoteTextSizeMin" /> + android:textSize="@dimen/singleCardNoteTextSizeMin" /> + android:textSize="@dimen/singleCardNoteTextSizeMin" /> + android:textSize="@dimen/singleCardNoteTextSizeMin" /> Date: Thu, 25 Mar 2021 12:33:40 +0000 Subject: [PATCH 71/77] Translated using Weblate (German) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/ --- app/src/main/res/values-de/strings.xml | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 103784282..2bb565b40 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ - Suche - Neu + Suchen + Hinzufügen Klicken Sie auf die Schaltfläche + (plus), um zuerst eine Karte hinzuzufügen. \n \nCatima trägt Ihre Karten auf dem Gerät, so dass sie immer in Reichweite sind. @@ -20,9 +20,9 @@ Aus der Favoritenliste entfernen Karte entfernen Bitte bestätigen Sie, dass diese Karte gelöscht werden soll. - Ok - Kopiere die Nummer in die Zwischenablage - Senden… + OK + Nummer in die Zwischenablage kopieren + Senden … Kundenkarte bearbeiten Neue Kundenkarte Strichcode scannen @@ -102,7 +102,7 @@ %d Karten Gruppen: %s - Kundenkartenanhänger + Loyalty Card Keychain Aus welcher App Daten importieren\? %s scheint kein gültiges Guthaben zu sein. Punkte @@ -127,4 +127,22 @@ \nWir sammeln KEINE DATEN und unsere Anwendung ist quelloffen, sodass jeder bestätigen kann, dass dies wahr ist. Annehmen Datenschutzrichtlinie + Bitte wählen Sie Ihre Voucher Vault-Exportdatei aus. Es heißt höchstwahrscheinlich vouchervault.json. +\n +\nEine Voucher Vault-Exportdatei kann durch Drücken von Exportieren erstellt werden. + Aus Voucher Vault importieren + Bitte wählen Sie Ihre Loyalty Card Keychain-Exportdatei aus. Es heißt höchstwahrscheinlich LoyaltyCardKeychain.csv. +\n +\nEine Loyalty Card Keychain-Exportdatei kann erstellt werden, indem Sie im Menü Import/Export auf Exportieren klicken. + Aus Loyalty Card Keychain importieren + Bitte wählen Sie Ihre Fidme-Exportdatei aus. Es wird höchstwahrscheinlich so etwas wie fidme-export-request-xxxxxx.zip genannt. +\n +\nEine Fidme-Exportdatei kann in der Fidme-Anwendung erstellt werden, indem Sie zu Ihrem Profil gehen, Datenschutz auswählen und dann auf Meine Daten extrahieren klicken. +\n +\nBitte beachten Sie, dass Fidme den Strichcode-Typ nicht in den Exportdaten speichert, sodass Sie die importierten Karten manuell bearbeiten müssen. + Aus Fidme importieren + Bitte wählen Sie Ihre Catima-Exportdatei aus. Es heißt höchstwahrscheinlich Catima.csv. +\n +\nEine Catima-Exportdatei kann erstellt werden, indem Sie im Menü Import/Export auf Exportieren klicken. + Aus Catima importieren \ No newline at end of file From 79a143ebaf2beed9e722dfa1c28b9940db212335 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Thu, 25 Mar 2021 12:33:18 +0000 Subject: [PATCH 72/77] Translated using Weblate (French) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/ --- app/src/main/res/values-fr/strings.xml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7213a3ca6..39abdc653 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -19,7 +19,7 @@ OK Copier le numéro dans le presse-papier Envoyer… - Modifier la carte de fidélité + Modifier la carte Ajouter une carte de fidélité Scanner le code-barres Raccourci de carte @@ -107,7 +107,7 @@ \n \nNous ne collectons AUCUNE DONNÉE et le code source de notre application est ouvert afin que tout le monde puisse le confirmer. Politique de confidentialité - Porte-clé de carte de fidélité + Loyalty Card Keychain Importer des données à partir de quelle application \? <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> ne semble pas être un solde valide. Points @@ -127,4 +127,22 @@ Expire : <xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">%s</xliff:g> Désactiver l\'écran de verrouillage pendant l\'affichage d\'une carte Garder l\'écran allumé pendant l\'affichage d\'une carte + Veuillez sélectionner votre fichier d\'exportation Voucher Vault. Il est probablement nommé vouchervault.json. +\n +\nUn fichier d\'exportation Voucher Vault peut être créé en appuyant sur Exporter. + Importer depuis Voucher Vault + Veuillez sélectionner votre fichier d\'exportation Loyalty Card Keychain . Il s\'appelle très probablement LoyaltyCardKeychain.csv. +\n +\nUn fichier d\'exportation Loyalty Card Keychain peut être créé en allant dans le menu Importer/Exporter et en appuyant sur Exporter. + Importer depuis Loyalty Card Keychain + Veuillez sélectionner votre fichier d\'exportation Fidme. Il est très probablement nommé quelque chose comme fidme-export-request-xxxxxx.zip. +\n +\nUn fichier d\'exportation Fidme peut être créé dans l\'application Fidme en accédant à votre profil, en choisissant Protection des données, puis en appuyant sur Extraire mes données. +\n +\nVeuillez noter que Fidme ne stocke pas le type de code-barres dans les données d\'exportation, vous devrez donc éditer les cartes importées manuellement. + Importer depuis Fidme + Veuillez sélectionner votre fichier d\'exportation Catima. Il est probablement nommé Catima.csv. +\n +\nUn fichier d\'exportation Catima peut être créé en allant dans le menu Importer/Exporter et en appuyant sur Exporter. + Importer depuis Catima \ No newline at end of file From d8b121f503171da9e6aaec65aa88523ea94d0c10 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Thu, 25 Mar 2021 12:33:58 +0000 Subject: [PATCH 73/77] Translated using Weblate (Italian) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/ --- app/src/main/res/values-it/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8d06d5911..02aca025a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -19,7 +19,7 @@ Sblocca rotazione Rimuovi carta fedeltà Conferma di voler eliminare questa carta. - Ok + OK Copia ID negli appunti Condividi Invia… @@ -119,7 +119,7 @@ Scaduta: %s Scade: %s Mantieni schermo acceso durante la visualizzazione di una carta - Portachiavi carta fedeltà + Loyalty Card Keychain Da quale app vuoi importare i dati\? Mantieni schermo attivo mentre visualizzi una carta Accetta From 7e9c7db8132366b13f3cdd467f4c3f7c01e64fee Mon Sep 17 00:00:00 2001 From: Alessandro Mandelli Date: Wed, 24 Mar 2021 19:50:16 +0000 Subject: [PATCH 74/77] Translated using Weblate (Italian) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/ --- app/src/main/res/values-it/strings.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 02aca025a..fce1c44d1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -127,4 +127,22 @@ \n \nNon raccogliamo ALCUN DATO e il codice sorgente della nostra applicazione è aperto, quindi chiunque può confermare che ciò sia vero. Informativa sulla riservatezza + Seleziona il tuo file di esportazione Voucher Vault. Molto probabilmente si chiama vouchervault.json. +\n +\nÈ possibile creare un file di esportazione Voucher Vault premendo Esporta. + Importa da Voucher Vault + Seleziona il file di esportazione Loyalty Card Keychain . Molto probabilmente si chiama LoyaltyCardKeychain.csv. +\n +\nÈ possibile creare un file di esportazione Loyalty Card Keychain accedendo al menu Importa / Esporta e premendo Esporta. + Importa da Loyalty Card Keychain + Seleziona il tuo file di esportazione Fidme. Molto probabilmente si chiama qualcosa come fidme-export-request-xxxxxx.zip. +\n +\nUn file di esportazione Fidme può essere creato nell\'app Fidme andando sul tuo profilo, scegliendo Protezione dati e quindi premendo Estrai i miei dati. +\n +\nTieni presente che Fidme non memorizza il tipo di codice a barre nei dati di esportazione, quindi dovrai modificare manualmente le carte importate. + Importa da Fidme + Seleziona il tuo file di esportazione Catima. Molto probabilmente si chiama Catima.csv. +\n +\nÈ possibile creare un file di esportazione Catima andando nel menu Importa / Esporta e premendo Esporta. + Importa da Catima \ No newline at end of file From 01025e862a805303476a8bbd54e5e2ec7313aaec Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Thu, 25 Mar 2021 11:08:50 +0000 Subject: [PATCH 75/77] Translated using Weblate (Dutch) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/ --- app/src/main/res/values-nl/strings.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fd0124cae..f770af54f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -127,4 +127,22 @@ \nWe verzamelen HELEMAAL NIKS. Bovendien is onze app open source, zodat een ieder met eigen ogen kan zien wat de app wel of niet doet. Privacybeleid Accepteren + Kies het gewenste Voucher Vault-exportbestand - normaliter heet dit bestand \'vouchervault.json\'. +\n +\nGa naar het Exportmenu om een Voucher Vault-exportbestand samen te stellen. + Importeren uit Voucher Vault + Kies het gewenste Klantenkaartkluis-exportbestand - normaliter heet dit bestand \'LoyaltyCardKeychain.csv\'. +\n +\nGa naar het Import-/Exportmenu om een Klantenkaartkluis-exportbestand samen te stellen. + Importeren uit Klantenkaartkluis + Kies het gewenste Fidme-exportbestand - normaliter heet dit bestand \'fidme-export-request-xxxxxx.zip\'. +\n +\nOpen de Fidme-app, ga naar je profiel en druk op \'Gegevensbescherming\' om een exportbestand samen te stellen. +\n +\nLet op: Fidme slaat de soorten barcodes niet op in het exportbestand, dus dat moet je na achteraf nog instellen. + Kies het gewenste Catima-exportbestand - normaliter heet dit bestand \'Catima.csv\'. +\n +\nGa naar het Import-/Exportmenu om een Catima-exportbestand samen te stellen. + Importeren uit Fidme + Importeren uit Catima \ No newline at end of file From 60a172813fee980f25964007a350ca0de89b168c Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 25 Mar 2021 06:57:44 +0000 Subject: [PATCH 76/77] Translated using Weblate (Russian) Currently translated at 100.0% (145 of 145 strings) Translation: Catima/Catima Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/ --- app/src/main/res/values-ru/strings.xml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ca6b48e74..f10e24a13 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -21,7 +21,7 @@ Удаление карты Подтвердите удаление карты. ОК - Скопировать номер карты в буфер обмена + Копировать номер карты Переслать Отправить… Изменить карту @@ -77,14 +77,14 @@ Максимальная яркость при показе карты Портретная ориентация экрана при показе карты Я хочу поделиться картой с вами - Данные карты экспортированы + Данные карт успешно экспортированы Все Нажмите кнопку + (плюс), чтобы добавить группы. \n \nГруппы упрощают поиск. Группы Введите название группы - Данные карты успешно импортированы + Данные карт успешно импортированы Звезда избранного На основе Loyalty Card Keychain, авторские права 2016–2020 Branden Archer. Удалить из избранного @@ -129,4 +129,22 @@ %d карт Принять + Выберите файл экспорта Voucher Vault. Обычно он называется \"vouchervault.json\". +\n +\nФайл экспорта Voucher Vault можно создать, нажав кнопку \"Экспорт\". + Импорт из Voucher Vault + Выберите файл экспорта Fidme. Обычно он называется как-то вроде \"fidme-export-request-xxxxxx.zip\". +\n +\nФайл экспорта Fidme можно создать в приложении Fidme, перейдя в свой профиль, выбрав функцию \"Защита данных\", и затем нажав кнопку \"Извлечь мои данные\". +\n +\nОбратите внимание, что Fidme не сохраняет тип штрих-кода в экспортированных данных, поэтому вам придётся редактировать импортированные данные карт вручную. + Выберите файл экспорта Loyalty Card Keychain. Обычно он называется \"LoyaltyCardKeychain.csv\". +\n +\nФайл экспорта Loyalty Card Keychain можно создать, перейдя в меню \"Импорт/Экспорт\" и нажав кнопку \"Экспорт\". + Импорт из Loyalty Card Keychain + Выберите файл экспорта Catima. Обычно он называется \"Catima.csv\". +\n +\nФайл экспорта Catima можно создать, перейдя в меню \"Импорт/Экспорт\" и нажав кнопку \"Экспорт\". + Импорт из Fidme + Импорт из Catima \ No newline at end of file From f76df0d25208fd9aec1999d5e978179e5f0a42f8 Mon Sep 17 00:00:00 2001 From: Alessandro Mandelli Date: Wed, 24 Mar 2021 19:47:07 +0000 Subject: [PATCH 77/77] Translated using Weblate (Italian) Currently translated at 100.0% (3 of 3 strings) Translation: Catima/Fastlane Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/ --- fastlane/metadata/android/it/full_description.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/it/full_description.txt diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt new file mode 100644 index 000000000..886015caa --- /dev/null +++ b/fastlane/metadata/android/it/full_description.txt @@ -0,0 +1,7 @@ +Sei stanco di cercare la tua carta fedeltà durante i pagamenti in negozio? Cerchi una soluzione gratuita che non catture le tue informazioni? + +Catima è un'applicazione che memorizzerà sul tuo telefono le tue carte fedeltà basate su codici a barre. L'applicazione è open source e cerca di fare bene una cosa: gestire le tue carte! + +Nuove carte possono essere aggiunte in un attimo. Utilizza la fotocamera per acquisire il codice a barre o digita il numero. Quando il codice a barre viene caricato nel negozio e visualizzato, può essere scansionato con un moderno lettore di codici a barre. (Alcuni negozi utilizzano lettori di codici a barre meno recenti, come scanner piani, invece di scanner di immagini. Questi non possono leggere il display dello smartphone. In questi casi, devi chiedere all'impiegato di digitare il numero manualmente). + +L'applicazione richiede pochissime autorizzazioni e non tenta mai di accedere a Internet. E' possibile eseguire il backup delle carte nella memoria locale. E da lì puoi conservare il backup da qualche parte al sicuro.