Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import de.davis.passwordmanager.database.ElementType;
import de.davis.passwordmanager.database.entities.details.ElementDetail;
import de.davis.passwordmanager.utils.CreditCardUtil;
import de.davis.passwordmanager.utils.card.Card;
import de.davis.passwordmanager.utils.card.CardFactory;

public class CreditCardDetails implements ElementDetail {

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

this.cardholder = cardholder;
this.cardNumber = cardNumber.replaceAll("\\s", "");
this.cardNumber = cardNumber;
this.cvv = cvv;
}

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

public String getCardNumber() {
return cardNumber;
return getCard().getRawNumber();
}

public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}

public String getSecretNumber(){
return getFormattedNumber().replaceAll("(\\d{4}\\s){3}", "**** **** **** ");
return getCard().mask();
}

public String getFormattedNumber(){
return CreditCardUtil.formatNumber(getCardNumber());
return getCard().getCardNumber();
}

public String getCvv() {
Expand All @@ -71,6 +73,10 @@ public ElementType getElementType() {
return ElementType.CREDIT_CARD;
}

public Card getCard(){
return CardFactory.INSTANCE.createFromCardNumber(cardNumber);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
package de.davis.passwordmanager.listeners.text;

import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.widget.EditText;

import java.util.function.Consumer;

import de.davis.passwordmanager.utils.CreditCardUtil;
import de.davis.passwordmanager.utils.card.CardType;
import de.davis.passwordmanager.utils.card.Formatter;

public class CreditCardNumberTextWatcher implements TextWatcher {

private final EditText cardNumberEditText;
private boolean changing;

private Consumer<CardType> onTypeDetected;

public CreditCardNumberTextWatcher(EditText cardNumberEditText) {
this.cardNumberEditText = cardNumberEditText;
}

public void setOnTypeDetected(Consumer<CardType> onTypeDetected) {
this.onTypeDetected = onTypeDetected;
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

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

changing = true;

s.replace(0, s.length(), CreditCardUtil.formatNumber(s.toString()));
CardType type = CardType.Companion.getTypeByNumber(s.toString());
int length = type.getLengthRange().getEndInclusive();
if(type.getFormatter() instanceof Formatter.FourDigitChunkFormatter)
length += Math.floorDiv(length - 1, 4);
else
length += 2; // FourSixRemainderChunkFormatter can only add up to 2 more spaces

String formatted = CreditCardUtil.formatNumber(s.toString());

if(formatted.length() > length)
formatted = formatted.substring(0, length);

cardNumberEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(length)});
if(onTypeDetected != null)
onTypeDetected.accept(type);


int selectionEnd = cardNumberEditText.getSelectionEnd();
boolean isLast = cardNumberEditText.length() == selectionEnd;

s.replace(0, s.length(), formatted);

if(isLast)
cardNumberEditText.setSelection(formatted.length());
else
cardNumberEditText.setSelection(Math.min(formatted.length(), selectionEnd));

changing = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ object SuggestedDatasetBuilder {
builder: (TextProvider, requestCode: Int) -> Dataset
): List<Dataset> = url?.let { url ->
SecureElementManager.getSecureElements(typeId)
.take(n)
.filter {
(it.detail as PasswordDetails).origin.couldBeUrl(url) ||
it.title.couldBeUrl(url)
}
.take(n)
.mapIndexed { index, element -> builder(element.getTextProvider(), index) }
} ?: emptyList()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails;
import de.davis.passwordmanager.database.entities.details.password.PasswordDetails;
import de.davis.passwordmanager.ui.views.OptionBottomSheet;
import de.davis.passwordmanager.utils.card.CardType;

public class SecureElementViewHolder extends BasicViewHolder<SecureElement> {

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

if(item.getElementType() == ElementType.PASSWORD){
type.setText(item.getElementType().getTitle());
info.setText(((PasswordDetails)item.getDetail()).getStrength().getString());
info.setTextColor(((PasswordDetails)item.getDetail()).getStrength().getColor(context));
}else{
CreditCardDetails details = (CreditCardDetails) item.getDetail();
setShortenedTextIfNeeded(info, details.getSecretNumber(), details.getSecretNumber().substring(15, 19));
CardType cardType = details.getCard().getType();
if(cardType == CardType.Unknown)
type.setText(item.getElementType().getTitle());
else
type.setText(cardType.name().replaceAll("([a-z])([A-Z])", "$1 $2"));

String secret = details.getSecretNumber();
setShortenedTextIfNeeded(info, secret, secret.substring(secret.lastIndexOf(" ")));
info.setTextColor(MaterialColors.getColor(itemView.getContext(), com.google.android.material.R.attr.colorOnSurface, Color.BLACK));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.graphics.Color;
import android.os.Bundle;
import android.provider.Settings;
import android.text.InputFilter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand Down Expand Up @@ -36,6 +37,7 @@
import de.davis.passwordmanager.text.method.CreditCardNumberTransformationMethod;
import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity;
import de.davis.passwordmanager.utils.CreditCardUtil;
import de.davis.passwordmanager.utils.card.CardType;

public class CreateCreditCardActivity extends CreateSecureElementActivity {

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

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

Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).addTextChangedListener(new CreditCardNumberTextWatcher());
binding.textInputLayoutCardNumber.getEditText().setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());
CreditCardNumberTextWatcher textWatcher = new CreditCardNumberTextWatcher(binding.cardNumber);
binding.cardNumber.addTextChangedListener(textWatcher);
binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());
binding.textInputLayoutCardNumber.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(binding.textInputLayoutCardNumber));


textWatcher.setOnTypeDetected(cardType -> {
int cvvMaxLength = cardType == CardType.AmericanExpress ? 4 : 3;
binding.cardCVV.setFilters(new InputFilter[]{new InputFilter.LengthFilter(cvvMaxLength)});

String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString();
binding.cardCVV.setText(cvv.subSequence(0, Math.min(cvv.length(), cvvMaxLength)));
});

nfcManager = new NfcManager(this) {
@Override
protected void cardReceived(EmvCard card, CommunicationException e) {
Expand Down Expand Up @@ -100,10 +112,10 @@ public void fillInElement(@NonNull SecureElement element) {
binding.textInputLayoutTitle.getEditText().setText(element.getTitle());

CreditCardDetails details = (CreditCardDetails) element.getDetail();
binding.textInputLayoutUsername.getEditText().setText(details.getCardholder().getFullName());
binding.textInputLayoutCardNumber.getEditText().setText(details.getCardNumber());
binding.textInputLayoutCardCVV.getEditText().setText(details.getCvv());
binding.textInputLayoutCardDate.getEditText().setText(details.getExpirationDate());
binding.cardHolder.setText(details.getCardholder().getFullName());
binding.cardNumber.setText(details.getCardNumber());
binding.cardCVV.setText(details.getCvv());
binding.expirationDate.setText(details.getExpirationDate());
}

@Override
Expand Down Expand Up @@ -154,8 +166,8 @@ public CreateSecureElementActivity.Result check() {
result.setSuccess(true);

String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString();
String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString();
String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString();
String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString();
String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString();

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

if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber)){
binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card_number_length));
if(!CreditCardUtil.isValidCardNumberLength(creditCardNumber) || !CreditCardUtil.isValidCheckSum(creditCardNumber)){
binding.textInputLayoutCardNumber.setError(getString(R.string.invalid_card));
result.setSuccess(false);
}else
binding.textInputLayoutCardNumber.setErrorEnabled(false);
Expand All @@ -192,11 +204,11 @@ public CreateSecureElementActivity.Result check() {
@Override
protected SecureElement toElement() {
String title = Objects.requireNonNull(binding.textInputLayoutTitle.getEditText()).getText().toString().trim();
String creditCardNumber = Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).getText().toString().trim();
String expiryDate = Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).getText().toString().trim();
String cvv = Objects.requireNonNull(binding.textInputLayoutCardCVV.getEditText()).getText().toString().trim();
String creditCardNumber = Objects.requireNonNull(binding.cardNumber.getText()).toString().trim();
String expiryDate = Objects.requireNonNull(binding.expirationDate.getText()).toString().trim();
String cvv = Objects.requireNonNull(binding.cardCVV.getText()).toString().trim();

Name name = Name.fromFullName(Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).getText().toString());
Name name = Name.fromFullName(Objects.requireNonNull(binding.cardHolder.getText()).toString());

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

Objects.requireNonNull(binding.textInputLayoutUsername.getEditText()).setText(name.getFullName());
Objects.requireNonNull(binding.textInputLayoutCardNumber.getEditText()).setText(cardNumber);
Objects.requireNonNull(binding.textInputLayoutCardDate.getEditText()).setText(expireString);
binding.cardHolder.setText(name.getFullName());
binding.cardNumber.setText(cardNumber);
binding.expirationDate.setText(expireString);
}

private void setNfcMessageSuccess(@StringRes int stringRes){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void fillInElement(@NonNull SecureElement creditCard) {
TextInputLayout til = view.findViewById(R.id.textInputLayout);
EditText et = til.getEditText();
et.setKeyListener(DigitsKeyListener.getInstance("0123456789 "));
et.addTextChangedListener(new CreditCardNumberTextWatcher());
et.addTextChangedListener(new CreditCardNumberTextWatcher(et));
til.setEndIconOnClickListener(new OnCreditCardEndIconClickListener(til));
});
binding.cardNumber.setTransformationMethod(CreditCardNumberTransformationMethod.getInstance());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import java.util.Date;
import java.util.Locale;

import de.davis.passwordmanager.utils.card.Card;
import de.davis.passwordmanager.utils.card.CardFactory;

public class CreditCardUtil {

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

String formatted = cardNumber.replaceAll("\\s", "");
Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber);

return card.isValidLength();
}

public static boolean isValidCheckSum(String cardNumber){
if(cardNumber == null)
return false;

Card card = CardFactory.INSTANCE.createFromCardNumber(cardNumber);

return formatted.length() == 16;
return card.isValidLuhnNumber();
}

public static String formatNumber(String s){
String f = s.replaceAll("\\s", "").replaceAll("\\d{4}", "$0 ");
return f.endsWith(" ") ? f.substring(0, f.length() -1) : f;
Card card = CardFactory.INSTANCE.createFromCardNumber(s);

return card.getCardNumber();
}

public static String formatDate(Date date){
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/de/davis/passwordmanager/utils/card/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.davis.passwordmanager.utils.card

import de.davis.passwordmanager.utils.card.algorithm.LuhnAlgorithm

class Card(val rawNumber: String, val type: CardType) {
val cardNumber: String = type.formatter.format(rawNumber)

fun mask() = type.formatter.format(rawNumber.replace("\\d(?=\\d{4})".toRegex(), "•"))

fun isValidLuhnNumber(): Boolean = LuhnAlgorithm.isValid(rawNumber)

fun isValidLength(): Boolean = type.lengthRange.contains(rawNumber.length)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.davis.passwordmanager.utils.card

object CardFactory {
fun createFromCardNumber(cardNumber: String): Card {
return Card(cardNumber.replace(" ", ""), CardType.getTypeByNumber(cardNumber))
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/de/davis/passwordmanager/utils/card/CardType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.davis.passwordmanager.utils.card

enum class CardType(
val prefixes: List<String>,
val lengthRange: IntRange,
val formatter: Formatter
) {
Visa(listOf("4"), 16..16, Formatter.FourDigitChunkFormatter),
MasterCard((1..5).map { "5$it" }, 16..16, Formatter.FourDigitChunkFormatter),
AmericanExpress(listOf("34", "37"), 15..15, Formatter.FourSixRemainderChunkFormatter),
Discover(listOf("6011", "65"), 16..16, Formatter.FourDigitChunkFormatter),
JCB(listOf("35"), 16..19, Formatter.FourDigitChunkFormatter),
DinnersClub(listOf("36", "38", "39"), 14..14, Formatter.FourSixRemainderChunkFormatter),

Unknown(emptyList(), 14..19, Formatter.FourDigitChunkFormatter);

companion object {
fun getTypeByNumber(cardNumber: String) = entries.firstOrNull {
it.prefixes.any { prefix -> cardNumber.startsWith(prefix) }
} ?: Unknown
}
}
26 changes: 26 additions & 0 deletions app/src/main/java/de/davis/passwordmanager/utils/card/Formatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.davis.passwordmanager.utils.card

sealed class Formatter {

data object FourDigitChunkFormatter : Formatter() {

override fun runFormat(cardNumber: String): String = cardNumber.chunked(4).joinToString(" ")
}


data object FourSixRemainderChunkFormatter : Formatter() {

override fun runFormat(cardNumber: String): String = when {
cardNumber.length <= 4 -> cardNumber
cardNumber.length <= 10 -> cardNumber.take(4) + " " + cardNumber.substring(4)
else -> cardNumber.substring(0, 4) + " " + cardNumber.substring(4, 10) +
" " + cardNumber.substring(10)
}
}

protected abstract fun runFormat(cardNumber: String): String

fun format(cardNumber: String): String {
return runFormat(cardNumber.replace(" ", ""))
}
}
Loading