Skip to content

Commit bdecb6c

Browse files
authored
[Merge] Feat/creditcard-brands
2 parents 3aeef60 + 6e9e946 commit bdecb6c

File tree

15 files changed

+195
-33
lines changed

15 files changed

+195
-33
lines changed

app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import de.davis.passwordmanager.database.ElementType;
77
import de.davis.passwordmanager.database.entities.details.ElementDetail;
88
import de.davis.passwordmanager.utils.CreditCardUtil;
9+
import de.davis.passwordmanager.utils.card.Card;
10+
import de.davis.passwordmanager.utils.card.CardFactory;
911

1012
public class CreditCardDetails implements ElementDetail {
1113

@@ -22,7 +24,7 @@ public CreditCardDetails(Name cardholder, String expirationDate, String cardNumb
2224
this.expirationDate = expirationDate;
2325

2426
this.cardholder = cardholder;
25-
this.cardNumber = cardNumber.replaceAll("\\s", "");
27+
this.cardNumber = cardNumber;
2628
this.cvv = cvv;
2729
}
2830

@@ -35,19 +37,19 @@ public void setCardholder(Name cardholder) {
3537
}
3638

3739
public String getCardNumber() {
38-
return cardNumber;
40+
return getCard().getRawNumber();
3941
}
4042

4143
public void setCardNumber(String cardNumber) {
4244
this.cardNumber = cardNumber;
4345
}
4446

4547
public String getSecretNumber(){
46-
return getFormattedNumber().replaceAll("(\\d{4}\\s){3}", "**** **** **** ");
48+
return getCard().mask();
4749
}
4850

4951
public String getFormattedNumber(){
50-
return CreditCardUtil.formatNumber(getCardNumber());
52+
return getCard().getCardNumber();
5153
}
5254

5355
public String getCvv() {
@@ -71,6 +73,10 @@ public ElementType getElementType() {
7173
return ElementType.CREDIT_CARD;
7274
}
7375

76+
public Card getCard(){
77+
return CardFactory.INSTANCE.createFromCardNumber(cardNumber);
78+
}
79+
7480
@Override
7581
public boolean equals(Object o) {
7682
if (this == o) return true;

app/src/main/java/de/davis/passwordmanager/listeners/text/CreditCardNumberTextWatcher.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
package de.davis.passwordmanager.listeners.text;
22

33
import android.text.Editable;
4+
import android.text.InputFilter;
45
import android.text.TextWatcher;
6+
import android.widget.EditText;
7+
8+
import java.util.function.Consumer;
59

610
import de.davis.passwordmanager.utils.CreditCardUtil;
11+
import de.davis.passwordmanager.utils.card.CardType;
12+
import de.davis.passwordmanager.utils.card.Formatter;
713

814
public class CreditCardNumberTextWatcher implements TextWatcher {
915

16+
private final EditText cardNumberEditText;
1017
private boolean changing;
1118

19+
private Consumer<CardType> onTypeDetected;
20+
21+
public CreditCardNumberTextWatcher(EditText cardNumberEditText) {
22+
this.cardNumberEditText = cardNumberEditText;
23+
}
24+
25+
public void setOnTypeDetected(Consumer<CardType> onTypeDetected) {
26+
this.onTypeDetected = onTypeDetected;
27+
}
28+
1229
@Override
1330
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
1431

@@ -22,7 +39,32 @@ public void afterTextChanged(Editable s) {
2239

2340
changing = true;
2441

25-
s.replace(0, s.length(), CreditCardUtil.formatNumber(s.toString()));
42+
CardType type = CardType.Companion.getTypeByNumber(s.toString());
43+
int length = type.getLengthRange().getEndInclusive();
44+
if(type.getFormatter() instanceof Formatter.FourDigitChunkFormatter)
45+
length += Math.floorDiv(length - 1, 4);
46+
else
47+
length += 2; // FourSixRemainderChunkFormatter can only add up to 2 more spaces
48+
49+
String formatted = CreditCardUtil.formatNumber(s.toString());
50+
51+
if(formatted.length() > length)
52+
formatted = formatted.substring(0, length);
53+
54+
cardNumberEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(length)});
55+
if(onTypeDetected != null)
56+
onTypeDetected.accept(type);
57+
58+
59+
int selectionEnd = cardNumberEditText.getSelectionEnd();
60+
boolean isLast = cardNumberEditText.length() == selectionEnd;
61+
62+
s.replace(0, s.length(), formatted);
63+
64+
if(isLast)
65+
cardNumberEditText.setSelection(formatted.length());
66+
else
67+
cardNumberEditText.setSelection(Math.min(formatted.length(), selectionEnd));
2668

2769
changing = false;
2870
}

app/src/main/java/de/davis/passwordmanager/services/autofill/builder/SuggestedDatasetBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ object SuggestedDatasetBuilder {
1414
builder: (TextProvider, requestCode: Int) -> Dataset
1515
): List<Dataset> = url?.let { url ->
1616
SecureElementManager.getSecureElements(typeId)
17-
.take(n)
1817
.filter {
1918
(it.detail as PasswordDetails).origin.couldBeUrl(url) ||
2019
it.title.couldBeUrl(url)
2120
}
21+
.take(n)
2222
.mapIndexed { index, element -> builder(element.getTextProvider(), index) }
2323
} ?: emptyList()
2424

app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails;
3030
import de.davis.passwordmanager.database.entities.details.password.PasswordDetails;
3131
import de.davis.passwordmanager.ui.views.OptionBottomSheet;
32+
import de.davis.passwordmanager.utils.card.CardType;
3233

3334
public class SecureElementViewHolder extends BasicViewHolder<SecureElement> {
3435

@@ -73,11 +74,19 @@ public void bindGeneral(@NonNull SecureElement item, String filter, OnItemClicke
7374
image.setImageDrawable(item.getIcon(context));
7475

7576
if(item.getElementType() == ElementType.PASSWORD){
77+
type.setText(item.getElementType().getTitle());
7678
info.setText(((PasswordDetails)item.getDetail()).getStrength().getString());
7779
info.setTextColor(((PasswordDetails)item.getDetail()).getStrength().getColor(context));
7880
}else{
7981
CreditCardDetails details = (CreditCardDetails) item.getDetail();
80-
setShortenedTextIfNeeded(info, details.getSecretNumber(), details.getSecretNumber().substring(15, 19));
82+
CardType cardType = details.getCard().getType();
83+
if(cardType == CardType.Unknown)
84+
type.setText(item.getElementType().getTitle());
85+
else
86+
type.setText(cardType.name().replaceAll("([a-z])([A-Z])", "$1 $2"));
87+
88+
String secret = details.getSecretNumber();
89+
setShortenedTextIfNeeded(info, secret, secret.substring(secret.lastIndexOf(" ")));
8190
info.setTextColor(MaterialColors.getColor(itemView.getContext(), com.google.android.material.R.attr.colorOnSurface, Color.BLACK));
8291
}
8392

app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.graphics.Color;
55
import android.os.Bundle;
66
import android.provider.Settings;
7+
import android.text.InputFilter;
78
import android.view.LayoutInflater;
89
import android.view.View;
910
import android.view.ViewGroup;
@@ -36,6 +37,7 @@
3637
import de.davis.passwordmanager.text.method.CreditCardNumberTransformationMethod;
3738
import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity;
3839
import de.davis.passwordmanager.utils.CreditCardUtil;
40+
import de.davis.passwordmanager.utils.card.CardType;
3941

4042
public class CreateCreditCardActivity extends CreateSecureElementActivity {
4143

@@ -63,10 +65,20 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
6365

6466
Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).addTextChangedListener(new ExpiryDateTextWatcher());
6567

66-
Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).addTextChangedListener(new CreditCardNumberTextWatcher());
67-
binding.textInputLayoutCardNumber.getEditText().setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());
68+
CreditCardNumberTextWatcher textWatcher = new CreditCardNumberTextWatcher(binding.cardNumber);
69+
binding.cardNumber.addTextChangedListener(textWatcher);
70+
binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());
6871
binding.textInputLayoutCardNumber.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(binding.textInputLayoutCardNumber));
6972

73+
74+
textWatcher.setOnTypeDetected(cardType -> {
75+
int cvvMaxLength = cardType == CardType.AmericanExpress ? 4 : 3;
76+
binding.cardCVV.setFilters(new InputFilter[]{new InputFilter.LengthFilter(cvvMaxLength)});
77+
78+
String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString();
79+
binding.cardCVV.setText(cvv.subSequence(0, Math.min(cvv.length(), cvvMaxLength)));
80+
});
81+
7082
nfcManager = new NfcManager(this) {
7183
@Override
7284
protected void cardReceived(EmvCard card, CommunicationException e) {
@@ -100,10 +112,10 @@ public void fillInElement(@NonNull SecureElement element) {
100112
binding.textInputLayoutTitle.getEditText().setText(element.getTitle());
101113

102114
CreditCardDetails details = (CreditCardDetails) element.getDetail();
103-
binding.textInputLayoutUsername.getEditText().setText(details.getCardholder().getFullName());
104-
binding.textInputLayoutCardNumber.getEditText().setText(details.getCardNumber());
105-
binding.textInputLayoutCardCVV.getEditText().setText(details.getCvv());
106-
binding.textInputLayoutCardDate.getEditText().setText(details.getExpirationDate());
115+
binding.cardHolder.setText(details.getCardholder().getFullName());
116+
binding.cardNumber.setText(details.getCardNumber());
117+
binding.cardCVV.setText(details.getCvv());
118+
binding.expirationDate.setText(details.getExpirationDate());
107119
}
108120

109121
@Override
@@ -154,8 +166,8 @@ public CreateSecureElementActivity.Result check() {
154166
result.setSuccess(true);
155167

156168
String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString();
157-
String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString();
158-
String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString();
169+
String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString();
170+
String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString();
159171

160172
if(title.isBlank()){
161173
binding.textInputLayoutTitle.setError(getString(R.string.is_not_filled_in));
@@ -169,8 +181,8 @@ public CreateSecureElementActivity.Result check() {
169181
}else
170182
binding.textInputLayoutCardNumber.setErrorEnabled(false);
171183

172-
if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber)){
173-
binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card_number_length));
184+
if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber) || !CreditCardUtil.isValidCheckSum(creditCardNumber)){
185+
binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card));
174186
result.setSuccess(false);
175187
}else
176188
binding.textInputLayoutCardNumber.setErrorEnabled(false);
@@ -192,11 +204,11 @@ public CreateSecureElementActivity.Result check() {
192204
@Override
193205
protected SecureElement toElement() {
194206
String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString().trim();
195-
String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString().trim();
196-
String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString().trim();
197-
String cvv = Objects.requireNonNull(binding.textInputLayoutCardCVV.getEditText()).getText().toString().trim();
207+
String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString().trim();
208+
String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString().trim();
209+
String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString().trim();
198210

199-
Name name = Name.fromFullName(Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).getText().toString());
211+
Name name = Name.fromFullName(Objects.requireNonNull(binding.cardHolder.getText()).toString());
200212

201213
CreditCardDetails details = new CreditCardDetails(name, expiryDate, creditCardNumber, cvv);
202214
SecureElement card = getElement() == null ?
@@ -220,9 +232,9 @@ private void insertCard(EmvCard card){
220232
String cardNumber = card.getCardNumber();
221233
String expireString = CreditCardUtil.formatDate(card.getExpireDate());
222234

223-
Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).setText(name.getFullName());
224-
Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).setText(cardNumber);
225-
Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).setText(expireString);
235+
binding.cardHolder.setText(name.getFullName());
236+
binding.cardNumber.setText(cardNumber);
237+
binding.expirationDate.setText(expireString);
226238
}
227239

228240
private void setNfcMessageSuccess(@StringRes int stringRes){

app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void fillInElement(@NonNull SecureElement creditCard) {
4747
TextInputLayout til = view.findViewById(R.id.textInputLayout);
4848
EditText et = til.getEditText();
4949
et.setKeyListener(DigitsKeyListener.getInstance("0123456789 "));
50-
et.addTextChangedListener(new CreditCardNumberTextWatcher());
50+
et.addTextChangedListener(new CreditCardNumberTextWatcher(et));
5151
til.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(til));
5252
});
5353
binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());

app/src/main/java/de/davis/passwordmanager/utils/CreditCardUtil.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import java.util.Date;
77
import java.util.Locale;
88

9+
import de.davis.passwordmanager.utils.card.Card;
10+
import de.davis.passwordmanager.utils.card.CardFactory;
11+
912
public class CreditCardUtil {
1013

1114
public static boolean isValidDateFormat(String formatted){
@@ -24,14 +27,24 @@ public static boolean isValidCardNumberLength(String cardNumber){
2427
if(cardNumber == null)
2528
return false;
2629

27-
String formatted = cardNumber.replaceAll("\\s", "");
30+
Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber);
31+
32+
return card.isValidLength();
33+
}
34+
35+
public static boolean isValidCheckSum(String cardNumber){
36+
if(cardNumber == null)
37+
return false;
38+
39+
Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber);
2840

29-
return formatted.length() == 16;
41+
return card.isValidLuhnNumber();
3042
}
3143

3244
public static String formatNumber(String s){
33-
String f = s.replaceAll("\\s", "").replaceAll("\\d{4}", "$0 ");
34-
return f.endsWith(" ") ? f.substring(0, f.length() -1) : f;
45+
Card card = CardFactory.INSTANCE.createFromCardNumber(s);
46+
47+
return card.getCardNumber();
3548
}
3649

3750
public static String formatDate(Date date){
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package de.davis.passwordmanager.utils.card
2+
3+
import de.davis.passwordmanager.utils.card.algorithm.LuhnAlgorithm
4+
5+
class Card(val rawNumber: String, val type: CardType) {
6+
val cardNumber: String = type.formatter.format(rawNumber)
7+
8+
fun mask() = type.formatter.format(rawNumber.replace("\\d(?=\\d{4})".toRegex(), ""))
9+
10+
fun isValidLuhnNumber(): Boolean = LuhnAlgorithm.isValid(rawNumber)
11+
12+
fun isValidLength(): Boolean = type.lengthRange.contains(rawNumber.length)
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package de.davis.passwordmanager.utils.card
2+
3+
object CardFactory {
4+
fun createFromCardNumber(cardNumber: String): Card {
5+
return Card(cardNumber.replace(" ", ""), CardType.getTypeByNumber(cardNumber))
6+
}
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package de.davis.passwordmanager.utils.card
2+
3+
enum class CardType(
4+
val prefixes: List<String>,
5+
val lengthRange: IntRange,
6+
val formatter: Formatter
7+
) {
8+
Visa(listOf("4"), 16..16, Formatter.FourDigitChunkFormatter),
9+
MasterCard((1..5).map { "5$it" }, 16..16, Formatter.FourDigitChunkFormatter),
10+
AmericanExpress(listOf("34", "37"), 15..15, Formatter.FourSixRemainderChunkFormatter),
11+
Discover(listOf("6011", "65"), 16..16, Formatter.FourDigitChunkFormatter),
12+
JCB(listOf("35"), 16..19, Formatter.FourDigitChunkFormatter),
13+
DinnersClub(listOf("36", "38", "39"), 14..14, Formatter.FourSixRemainderChunkFormatter),
14+
15+
Unknown(emptyList(), 14..19, Formatter.FourDigitChunkFormatter);
16+
17+
companion object {
18+
fun getTypeByNumber(cardNumber: String) = entries.firstOrNull {
19+
it.prefixes.any { prefix -> cardNumber.startsWith(prefix) }
20+
} ?: Unknown
21+
}
22+
}

0 commit comments

Comments
 (0)