diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java index bd8d2c816..6b829e10a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactRepository.java @@ -93,66 +93,7 @@ public class SharedContactRepository { try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) { VCard vcard = Ezvcard.parse(stream).first(); - - ezvcard.property.StructuredName vName = vcard.getStructuredName(); - List vPhones = vcard.getTelephoneNumbers(); - List vEmails = vcard.getEmails(); - List vPostalAddresses = vcard.getAddresses(); - - String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null; - String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null; - - if (displayName == null && vName != null) { - displayName = vName.getGiven(); - } - - if (displayName == null && vcard.getOrganization() != null) { - displayName = organization; - } - - if (displayName == null) { - throw new IOException("No valid name."); - } - - Name name = new Name(displayName, - vName != null ? vName.getGiven() : null, - vName != null ? vName.getFamily() : null, - vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null, - vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null, - null); - - - List phoneNumbers = new ArrayList<>(vPhones.size()); - for (ezvcard.property.Telephone vEmail : vPhones) { - String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null; - - // 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; - phoneNumbers.add(new Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label)); - } - - List emails = new ArrayList<>(vEmails.size()); - for (ezvcard.property.Email vEmail : vEmails) { - String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null; - emails.add(new Email(vEmail.getValue(), emailTypeFromVcardType(label), label)); - } - - List postalAddresses = new ArrayList<>(vPostalAddresses.size()); - for (ezvcard.property.Address vPostalAddress : vPostalAddresses) { - String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null; - postalAddresses.add(new PostalAddress(postalAddressTypeFromVcardType(label), - label, - vPostalAddress.getStreetAddress(), - vPostalAddress.getPoBox(), - null, - vPostalAddress.getLocality(), - vPostalAddress.getRegion(), - vPostalAddress.getPostalCode(), - vPostalAddress.getCountry())); - } - - contact = new Contact(name, organization, phoneNumbers, emails, postalAddresses, null); + contact = VCardUtil.getContactFromVcard(vcard); } catch (IOException e) { Log.w(TAG, "Failed to parse the vcard.", e); } @@ -201,7 +142,7 @@ public class SharedContactRepository { String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber); Phone existing = numberMap.get(number); - Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel); + Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(cursorType), cursorLabel); if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) { numberMap.put(number, candidate); @@ -224,7 +165,7 @@ public class SharedContactRepository { int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE)); String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL)); - emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel)); + emails.add(new Email(cursorEmail, VCardUtil.emailTypeFromContactType(cursorType), cursorLabel)); } } @@ -247,7 +188,7 @@ public class SharedContactRepository { String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)); String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)); - postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType), + postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType), cursorLabel, cursorStreet, cursorPoBox, @@ -304,70 +245,6 @@ public class SharedContactRepository { return null; } - private Phone.Type phoneTypeFromContactType(int type) { - switch (type) { - case ContactsContract.CommonDataKinds.Phone.TYPE_HOME: - return Phone.Type.HOME; - case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: - return Phone.Type.MOBILE; - case ContactsContract.CommonDataKinds.Phone.TYPE_WORK: - return Phone.Type.WORK; - } - return Phone.Type.CUSTOM; - } - - private Phone.Type phoneTypeFromVcardType(@Nullable String type) { - if ("home".equalsIgnoreCase(type)) return Phone.Type.HOME; - else if ("cell".equalsIgnoreCase(type)) return Phone.Type.MOBILE; - else if ("work".equalsIgnoreCase(type)) return Phone.Type.WORK; - else return Phone.Type.CUSTOM; - } - - private Email.Type emailTypeFromContactType(int type) { - switch (type) { - case ContactsContract.CommonDataKinds.Email.TYPE_HOME: - return Email.Type.HOME; - case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE: - return Email.Type.MOBILE; - case ContactsContract.CommonDataKinds.Email.TYPE_WORK: - return Email.Type.WORK; - } - return Email.Type.CUSTOM; - } - - private Email.Type emailTypeFromVcardType(@Nullable String type) { - if ("home".equalsIgnoreCase(type)) return Email.Type.HOME; - else if ("cell".equalsIgnoreCase(type)) return Email.Type.MOBILE; - else if ("work".equalsIgnoreCase(type)) return Email.Type.WORK; - else return Email.Type.CUSTOM; - } - - private PostalAddress.Type postalAddressTypeFromContactType(int type) { - switch (type) { - case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME: - return PostalAddress.Type.HOME; - case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK: - return PostalAddress.Type.WORK; - } - return PostalAddress.Type.CUSTOM; - } - - private PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) { - if ("home".equalsIgnoreCase(type)) return PostalAddress.Type.HOME; - else if ("work".equalsIgnoreCase(type)) return PostalAddress.Type.WORK; - else return PostalAddress.Type.CUSTOM; - } - - private String getCleanedVcardType(@Nullable String type) { - if (TextUtils.isEmpty(type)) return ""; - - if (type.startsWith("x-") && type.length() > 2) { - return type.substring(2); - } - - return type; - } - interface ValueCallback { void onComplete(@NonNull T value); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java new file mode 100644 index 000000000..e4400080d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/VCardUtil.java @@ -0,0 +1,160 @@ +package org.thoughtcrime.securesms.contactshare; + +import android.provider.ContactsContract; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.logging.Log; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import ezvcard.Ezvcard; +import ezvcard.VCard; + +public final class VCardUtil { + + private VCardUtil(){} + + private static final String TAG = VCardUtil.class.getSimpleName(); + + public static List parseContacts(@NonNull String vCardData) { + List vContacts = Ezvcard.parse(vCardData).all(); + List 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 vPhones = vcard.getTelephoneNumbers(); + List vEmails = vcard.getEmails(); + List vPostalAddresses = vcard.getAddresses(); + + String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null; + String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null; + + if (displayName == null && vName != null) { + displayName = vName.getGiven(); + } + + if (displayName == null && vcard.getOrganization() != null) { + displayName = organization; + } + + if (displayName == null) { + Log.w(TAG, "Failed to parse the vcard: No valid name."); + return null; + } + + Contact.Name name = new Contact.Name(displayName, + vName != null ? vName.getGiven() : null, + vName != null ? vName.getFamily() : null, + vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null, + vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null, + null); + + + List phoneNumbers = new ArrayList<>(vPhones.size()); + for (ezvcard.property.Telephone vEmail : vPhones) { + String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null; + + // 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; + phoneNumbers.add(new Contact.Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label)); + } + + List emails = new ArrayList<>(vEmails.size()); + for (ezvcard.property.Email vEmail : vEmails) { + String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null; + emails.add(new Contact.Email(vEmail.getValue(), emailTypeFromVcardType(label), label)); + } + + List postalAddresses = new ArrayList<>(vPostalAddresses.size()); + for (ezvcard.property.Address vPostalAddress : vPostalAddresses) { + String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null; + postalAddresses.add(new Contact.PostalAddress(postalAddressTypeFromVcardType(label), + label, + vPostalAddress.getStreetAddress(), + vPostalAddress.getPoBox(), + null, + vPostalAddress.getLocality(), + vPostalAddress.getRegion(), + vPostalAddress.getPostalCode(), + vPostalAddress.getCountry())); + } + + return new Contact(name, organization, phoneNumbers, emails, postalAddresses, null); + } + + static Contact.Phone.Type phoneTypeFromContactType(int type) { + switch (type) { + case ContactsContract.CommonDataKinds.Phone.TYPE_HOME: + return Contact.Phone.Type.HOME; + case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: + return Contact.Phone.Type.MOBILE; + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK: + return Contact.Phone.Type.WORK; + } + return Contact.Phone.Type.CUSTOM; + } + + private static Contact.Phone.Type phoneTypeFromVcardType(@Nullable String type) { + if ("home".equalsIgnoreCase(type)) return Contact.Phone.Type.HOME; + else if ("cell".equalsIgnoreCase(type)) return Contact.Phone.Type.MOBILE; + else if ("work".equalsIgnoreCase(type)) return Contact.Phone.Type.WORK; + else return Contact.Phone.Type.CUSTOM; + } + + static Contact.Email.Type emailTypeFromContactType(int type) { + switch (type) { + case ContactsContract.CommonDataKinds.Email.TYPE_HOME: + return Contact.Email.Type.HOME; + case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE: + return Contact.Email.Type.MOBILE; + case ContactsContract.CommonDataKinds.Email.TYPE_WORK: + return Contact.Email.Type.WORK; + } + return Contact.Email.Type.CUSTOM; + } + + private static Contact.Email.Type emailTypeFromVcardType(@Nullable String type) { + if ("home".equalsIgnoreCase(type)) return Contact.Email.Type.HOME; + else if ("cell".equalsIgnoreCase(type)) return Contact.Email.Type.MOBILE; + else if ("work".equalsIgnoreCase(type)) return Contact.Email.Type.WORK; + else return Contact.Email.Type.CUSTOM; + } + + static Contact.PostalAddress.Type postalAddressTypeFromContactType(int type) { + switch (type) { + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME: + return Contact.PostalAddress.Type.HOME; + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK: + return Contact.PostalAddress.Type.WORK; + } + return Contact.PostalAddress.Type.CUSTOM; + } + + private static Contact.PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) { + if ("home".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.HOME; + else if ("work".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.WORK; + else return Contact.PostalAddress.Type.CUSTOM; + } + + private static String getCleanedVcardType(@Nullable String type) { + if (TextUtils.isEmpty(type)) return ""; + + if (type.startsWith("x-") && type.length() > 2) { + return type.substring(2); + } + + return type; + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 66d90f6d2..b16d09f41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -336,6 +336,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati } } + if (hasSharedContact(messageRecord)) { + int contactWidth = sharedContactStub.get().getMeasuredWidth(); + int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get()); + + if (contactWidth != availableWidth) { + sharedContactStub.get().getLayoutParams().width = availableWidth; + needsMeasure = true; + } + } + ConversationItemFooter activeFooter = getActiveFooter(messageRecord); int availableWidth = getAvailableMessageBubbleWidth(footer); @@ -892,12 +902,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati } private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional previous, @NonNull Optional next, boolean isGroupThread) { - if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) { - sharedContactStub.get().setSingularStyle(); - } else if (current.isOutgoing()) { - sharedContactStub.get().setClusteredOutgoingStyle(); - } else { - sharedContactStub.get().setClusteredIncomingStyle(); + if (TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))){ + if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) { + sharedContactStub.get().setSingularStyle(); + } else if (current.isOutgoing()) { + sharedContactStub.get().setClusteredOutgoingStyle(); + } else { + sharedContactStub.get().setClusteredIncomingStyle(); + } } } @@ -1075,7 +1087,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) { if (hasSticker(messageRecord) || isBorderless(messageRecord)) { return stickerFooter; - } else if (hasSharedContact(messageRecord)) { + } else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) { return sharedContactStub.get().getFooter(); } else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) { return mediaThumbnailStub.get().getFooter(); @@ -1442,7 +1454,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati Log.i(TAG, "Public URI: " + publicUri); Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType()); + intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), Intent.normalizeMimeType(slide.getContentType())); try { context.startActivity(intent); } catch (ActivityNotFoundException anfe) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 8941ac790..f0a7d24b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -13,6 +13,9 @@ import com.google.android.mms.pdu_alt.RetrieveConf; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.contactshare.Contact; +import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.contactshare.VCardUtil; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; @@ -33,6 +36,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; @@ -189,6 +193,7 @@ public class MmsDownloadJob extends BaseJob { Set members = new HashSet<>(); String body = null; List attachments = new LinkedList<>(); + List sharedContacts = new LinkedList<>(); RecipientId from = null; @@ -223,14 +228,18 @@ public class MmsDownloadJob extends BaseJob { PduPart part = media.getPart(i); if (part.getData() != null) { - Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory(); - String name = null; + if (Util.toIsoString(part.getContentType()).toLowerCase().equals(MediaUtil.VCARD)){ + sharedContacts.addAll(VCardUtil.parseContacts(new String(part.getData()))); + } else { + Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory(); + String name = null; - if (part.getName() != null) name = Util.toIsoString(part.getName()); + if (part.getName() != null) name = Util.toIsoString(part.getName()); - attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), - AttachmentDatabase.TRANSFER_PROGRESS_DONE, - part.getData().length, name, false, false, false, null, null, null, null, null)); + attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), + AttachmentDatabase.TRANSFER_PROGRESS_DONE, + part.getData().length, name, false, false, false, null, null, null, null, null)); + } } } } @@ -240,7 +249,7 @@ public class MmsDownloadJob extends BaseJob { group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients)); } - IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false); + IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts)); Optional insertResult = database.insertMessageInbox(message, contentLocation, threadId); if (insertResult.isPresent()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index b7fc59f38..324e638a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -48,7 +48,8 @@ public class IncomingMediaMessage { long expiresIn, boolean expirationUpdate, boolean viewOnce, - boolean unidentified) + boolean unidentified, + Optional> sharedContacts) { this.from = from; this.groupId = groupId.orNull(); @@ -64,6 +65,8 @@ public class IncomingMediaMessage { this.unidentified = unidentified; this.attachments.addAll(attachments); + this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList())); + } public IncomingMediaMessage(@NonNull RecipientId from, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java index 340cef1b9..cd3fa889f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartParser.java @@ -9,17 +9,20 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.Util; import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.List; public class PartParser { - private static final String TAG = Log.tag(PartParser.class); + private static final String TAG = Log.tag(PartParser.class); + private static final List DOCUMENT_TYPES = Arrays.asList("text/vcard", "text/x-vcard"); public static String getMessageText(PduBody body) { String bodyText = null; for (int i=0;i fileName = getFileName(); if (fileName.isPresent()) { - return Optional.of(getFileType(fileName)); + String fileType = getFileType(fileName); + if (!fileType.isEmpty()) { + return Optional.of(fileType); + } } return Optional.fromNullable(MediaUtil.getExtension(context, getUri()));