Do not use reflection to parse vCard.

This commit is contained in:
Cody Henthorne
2026-09-23 16:00:23 -04:00
committed by Michelle Tang
parent fea2359815
commit ea5df6ad9e
2 changed files with 34 additions and 17 deletions
@@ -9,10 +9,8 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import ezvcard.Ezvcard;
import ezvcard.VCard;
public final class VCardUtil {
@@ -21,15 +19,6 @@ public final class VCardUtil {
private static final String TAG = Log.tag(VCardUtil.class);
public static List<Contact> parseContacts(@NonNull String vCardData) {
List<VCard> vContacts = Ezvcard.parse(vCardData).all();
List<Contact> contacts = new LinkedList<>();
for (VCard vCard: vContacts){
contacts.add(getContactFromVcard(vCard));
}
return contacts;
}
static @Nullable Contact getContactFromVcard(@NonNull VCard vcard) {
ezvcard.property.StructuredName vName = vcard.getStructuredName();
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
@@ -62,24 +51,24 @@ public final class VCardUtil {
List<Contact.Phone> phoneNumbers = new ArrayList<>(vPhones.size());
for (ezvcard.property.Telephone vEmail : vPhones) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
for (ezvcard.property.Telephone vPhone : vPhones) {
String label = getVcardTypeLabel(vPhone);
// Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
String phoneNumberFromText = vEmail.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
String phoneNumberFromText = vPhone.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vPhone.getUri().getNumber() : phoneNumberFromText;
phoneNumbers.add(new Contact.Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
}
List<Contact.Email> emails = new ArrayList<>(vEmails.size());
for (ezvcard.property.Email vEmail : vEmails) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
String label = getVcardTypeLabel(vEmail);
emails.add(new Contact.Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
}
List<Contact.PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
String label = getVcardTypeLabel(vPostalAddress);
postalAddresses.add(new Contact.PostalAddress(postalAddressTypeFromVcardType(label),
label,
vPostalAddress.getStreetAddress(),
@@ -148,6 +137,12 @@ public final class VCardUtil {
else return Contact.PostalAddress.Type.CUSTOM;
}
private static @Nullable String getVcardTypeLabel(@NonNull ezvcard.property.VCardProperty property) {
String type = property.getParameters().getType();
return type != null ? getCleanedVcardType(type) : null;
}
private static String getCleanedVcardType(@Nullable String type) {
if (TextUtils.isEmpty(type)) return "";
@@ -89,6 +89,28 @@ class ContactCardReaderTest {
assertThat(contact.phoneNumbers.first().number).isEqualTo("+15105550101")
}
@Test
fun `vcard types are read off the raw parameter`() {
givenVcard(
"""
BEGIN:VCARD
VERSION:3.0
FN:Paige Hall
TEL;TYPE=WORK:+15105550101
TEL;TYPE=X-ASSISTANT:+15105550102
EMAIL;TYPE=HOME:paige@example.com
END:VCARD
""".trimIndent()
)
val contact = reader.readVCard(VCARD_URI)!!
assertThat(contact.phoneNumbers[0].type).isEqualTo(Contact.Phone.Type.WORK)
assertThat(contact.phoneNumbers[1].type).isEqualTo(Contact.Phone.Type.CUSTOM)
assertThat(contact.phoneNumbers[1].label).isEqualTo("X-ASSISTANT")
assertThat(contact.emails[0].type).isEqualTo(Contact.Email.Type.HOME)
}
private fun givenVcard(body: String) {
every { PartAuthority.getAttachmentStream(any(), any()) } returns ByteArrayInputStream(body.toByteArray())
}