From c705790d26483ed5acbe1e77a498698dfcd582cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asiel=20D=C3=ADaz=20Ben=C3=ADtez?= Date: Sun, 19 May 2024 22:22:39 +0200 Subject: [PATCH] display vcard view type and allow to import/export contacts as vcard (#3043) * implement Rpc API for parse_vcard * allow to create Recipient from VcardContact * display vcard view-type in incoming/outgoing messages * fix linter warnings in Recipient class * properly show vcard view-type in quoted messages * display fallback avatar in quotes * allow to attach vcard * implement basic click listener * timestamp type got changed from float to int * set stick translation for "contact" * share contact as vcard using the new Rpc.makeVcard API * allow to import contact when clicking vcard --- ...sation_activity_attachment_editor_stub.xml | 11 +++ res/layout/conversation_item_received.xml | 10 +++ res/layout/conversation_item_sent.xml | 10 +++ res/layout/conversation_item_vcard.xml | 10 +++ res/layout/vcard_view.xml | 54 +++++++++++++ src/com/b44t/messenger/DcMsg.java | 1 + src/com/b44t/messenger/rpc/Rpc.java | 15 ++++ src/com/b44t/messenger/rpc/VcardContact.java | 60 +++++++++++++++ .../securesms/AttachContactActivity.java | 15 +--- .../securesms/ConversationActivity.java | 62 +++++---------- .../securesms/ConversationItem.java | 77 ++++++++++++++++++- .../securesms/attachments/Attachment.java | 43 +++++++++++ .../securesms/attachments/DcAttachment.java | 7 ++ .../securesms/components/QuoteView.java | 23 +++++- .../securesms/components/VcardView.java | 75 ++++++++++++++++++ .../securesms/connect/DcHelper.java | 1 + .../avatars/GeneratedContactPhoto.java | 22 +++--- .../contacts/avatars/VcardContactPhoto.java | 45 +++++++++++ .../securesms/mms/AttachmentManager.java | 27 ++++++- src/org/thoughtcrime/securesms/mms/Slide.java | 4 + .../securesms/mms/VcardSlide.java | 26 +++++++ .../securesms/recipients/Recipient.java | 57 ++++++++------ .../securesms/util/MediaUtil.java | 6 ++ 23 files changed, 565 insertions(+), 96 deletions(-) create mode 100644 res/layout/conversation_item_vcard.xml create mode 100644 res/layout/vcard_view.xml create mode 100644 src/com/b44t/messenger/rpc/VcardContact.java create mode 100644 src/org/thoughtcrime/securesms/components/VcardView.java create mode 100644 src/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java create mode 100644 src/org/thoughtcrime/securesms/mms/VcardSlide.java diff --git a/res/layout/conversation_activity_attachment_editor_stub.xml b/res/layout/conversation_activity_attachment_editor_stub.xml index 40b60e5f6..81c2b18c5 100644 --- a/res/layout/conversation_activity_attachment_editor_stub.xml +++ b/res/layout/conversation_activity_attachment_editor_stub.xml @@ -69,6 +69,17 @@ android:padding="8dp" android:background="@drawable/message_bubble_background_sent_alone"/> + + diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 5c9e8ad78..c5654a094 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -153,6 +153,16 @@ android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" android:layout_marginRight="@dimen/message_bubble_horizontal_padding" /> + + + + + diff --git a/res/layout/vcard_view.xml b/res/layout/vcard_view.xml new file mode 100644 index 000000000..45b500c4d --- /dev/null +++ b/res/layout/vcard_view.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/src/com/b44t/messenger/DcMsg.java b/src/com/b44t/messenger/DcMsg.java index 31999c1c6..52a4c110d 100644 --- a/src/com/b44t/messenger/DcMsg.java +++ b/src/com/b44t/messenger/DcMsg.java @@ -20,6 +20,7 @@ public class DcMsg { public final static int DC_MSG_FILE = 60; public final static int DC_MSG_VIDEOCHAT_INVITATION = 70; public final static int DC_MSG_WEBXDC = 80; + public final static int DC_MSG_VCARD = 90; public final static int DC_INFO_UNKNOWN = 0; public final static int DC_INFO_GROUP_NAME_CHANGED = 2; diff --git a/src/com/b44t/messenger/rpc/Rpc.java b/src/com/b44t/messenger/rpc/Rpc.java index 31b6d3c43..88a65fbd4 100644 --- a/src/com/b44t/messenger/rpc/Rpc.java +++ b/src/com/b44t/messenger/rpc/Rpc.java @@ -8,6 +8,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -95,6 +96,20 @@ public class Rpc { return gson.fromJson(getResult("get_system_info"), mapType.getType()); } + public List parseVcard(String path) throws RpcException { + TypeToken> listType = new TypeToken>(){}; + return gson.fromJson(getResult("parse_vcard", path), listType.getType()); + } + + public String makeVcard(int accountId, int... contacts) throws RpcException { + return gson.fromJson(getResult("make_vcard", accountId, contacts), String.class); + } + + public List importVcard(int accountId, String path) throws RpcException { + TypeToken> listType = new TypeToken>(){}; + return gson.fromJson(getResult("import_vcard", accountId, path), listType.getType()); + } + public HttpResponse getHttpResponse(int accountId, String url) throws RpcException { return gson.fromJson(getResult("get_http_response", accountId, url), HttpResponse.class); } diff --git a/src/com/b44t/messenger/rpc/VcardContact.java b/src/com/b44t/messenger/rpc/VcardContact.java new file mode 100644 index 000000000..75c47ea0a --- /dev/null +++ b/src/com/b44t/messenger/rpc/VcardContact.java @@ -0,0 +1,60 @@ +package com.b44t.messenger.rpc; + +import android.util.Base64; + +public class VcardContact { + // Email address. + private final String addr; + + // The contact's name, or the email address if no name was given. + private final String displayName; + + // Public PGP key in Base64. + private final String key; + + // Profile image in Base64. + private final String profileImage; + + // Contact color in HTML color format. + private final String color; + + // Last update timestamp. + private final int timestamp; + + public VcardContact(String addr, String displayName, String key, String profileImage, String color, int timestamp) { + this.addr = addr; + this.displayName = displayName; + this.key = key; + this.profileImage = profileImage; + this.color = color; + this.timestamp = timestamp; + } + + public String getAddr() { + return addr; + } + + public String getDisplayName() { + return displayName; + } + + public byte[] getKey() { + return key == null? null : Base64.decode(key, Base64.NO_WRAP | Base64.NO_PADDING); + } + + public boolean hasProfileImage() { + return profileImage != null; + } + + public byte[] getProfileImage() { + return profileImage == null? null : Base64.decode(profileImage, Base64.NO_WRAP | Base64.NO_PADDING); + } + + public String getColor() { + return color; + } + + public int getTimestamp() { + return timestamp; + } +} diff --git a/src/org/thoughtcrime/securesms/AttachContactActivity.java b/src/org/thoughtcrime/securesms/AttachContactActivity.java index 1cdbf9c81..fcacec7b5 100644 --- a/src/org/thoughtcrime/securesms/AttachContactActivity.java +++ b/src/org/thoughtcrime/securesms/AttachContactActivity.java @@ -2,26 +2,17 @@ package org.thoughtcrime.securesms; import android.content.Intent; -import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.connect.DcHelper; public class AttachContactActivity extends ContactSelectionActivity { - public static final String NAME_EXTRA = "name_extra"; - public static final String ADDR_EXTRA = "addr_extra"; + public static final String CONTACT_ID_EXTRA = "contact_id_extra"; @Override public void onContactSelected(int specialId, String addr) { - String name = ""; - DcContext dcContext = DcHelper.getContext(this); - int contactId = dcContext.lookupContactIdByAddr(addr); - if (contactId != 0) { - name = dcContext.getContact(contactId).getDisplayName(); - } Intent intent = new Intent(); - intent.putExtra(NAME_EXTRA, name); - intent.putExtra(ADDR_EXTRA, addr); + int contactId = DcHelper.getContext(this).lookupContactIdByAddr(addr); + intent.putExtra(CONTACT_ID_EXTRA, contactId); setResult(RESULT_OK, intent); finish(); } diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 73de6faa1..ae6552223 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -71,6 +71,8 @@ import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; +import com.b44t.messenger.rpc.Rpc; +import com.b44t.messenger.rpc.RpcException; import com.b44t.messenger.util.concurrent.ListenableFuture; import com.b44t.messenger.util.concurrent.SettableFuture; @@ -103,7 +105,6 @@ import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.permissions.Permissions; @@ -125,12 +126,7 @@ import org.thoughtcrime.securesms.video.recode.VideoRecoder; import org.thoughtcrime.securesms.videochat.VideochatUtil; import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; @@ -193,6 +189,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private ApplicationContext context; private Recipient recipient; private DcContext dcContext; + private Rpc rpc; private DcChat dcChat = new DcChat(0, 0); private int chatId; private final boolean isSecureText = true; @@ -204,6 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity protected void onCreate(Bundle state, boolean ready) { this.context = ApplicationContext.getInstance(getApplicationContext()); this.dcContext = DcHelper.getContext(context); + this.rpc = DcHelper.getRpc(context); supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); setContentView(R.layout.conversation_activity); @@ -978,9 +976,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void addAttachmentContactInfo(Intent data) { - String name = data.getStringExtra(AttachContactActivity.NAME_EXTRA); - String mail = data.getStringExtra(AttachContactActivity.ADDR_EXTRA); - composeText.append(name + "\n" + mail); + int contactId = data.getIntExtra(AttachContactActivity.CONTACT_ID_EXTRA, 0); + if (contactId == 0) { + return; + } + + try { + byte[] vcard = rpc.makeVcard(dcContext.getAccountId(), contactId).getBytes(); + String mimeType = "application/octet-stream"; + setMedia(PersistentBlobProvider.getInstance().create(this, vcard, mimeType, "vcard.vcf"), MediaType.DOCUMENT); + } catch (RpcException e) { + Log.e(TAG, "makeVcard() failed", e); + } } private boolean isMultiUser() { @@ -991,39 +998,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity return dcChat.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED; } - public static String getRealPathFromAttachment(Context context, Attachment attachment) { - try { - // get file in the blobdir as `/[-].` - String filename = attachment.getFileName(); - String ext = ""; - if(filename==null) { - filename = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date()); - ext = "." + MediaUtil.getExtensionFromMimeType(attachment.getContentType()); - } - else { - int i = filename.lastIndexOf("."); - if(i>=0) { - ext = filename.substring(i); - filename = filename.substring(0, i); - } - } - String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext); - - // copy content to this file - if(path!=null) { - InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getDataUri()); - OutputStream outputStream = new FileOutputStream(path); - Util.copy(inputStream, outputStream); - } - - return path; - } - catch(Exception e) { - e.printStackTrace(); - return null; - } - } - //////// send message or save draft protected static final int ACTION_SEND_OUT = 1; @@ -1076,7 +1050,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } else { msg = new DcMsg(dcContext, DcMsg.DC_MSG_FILE); } - String path = getRealPathFromAttachment(this, attachment); + String path = attachment.getRealPath(this); msg.setFile(path, null); } } @@ -1336,7 +1310,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void sendSticker(@NonNull Uri uri, String contentType) { Attachment attachment = new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, null, null, false); - String path = getRealPathFromAttachment(this, attachment); + String path = attachment.getRealPath(this); Optional quote = inputPanel.getQuote(); inputPanel.clearQuote(); diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 6be6d1912..34d6ab91e 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -32,16 +32,19 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcMsg; import com.b44t.messenger.rpc.Reactions; import com.b44t.messenger.rpc.RpcException; +import com.b44t.messenger.rpc.VcardContact; import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.components.AudioView; @@ -51,6 +54,7 @@ import org.thoughtcrime.securesms.components.ConversationItemFooter; import org.thoughtcrime.securesms.components.ConversationItemThumbnail; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.QuoteView; +import org.thoughtcrime.securesms.components.VcardView; import org.thoughtcrime.securesms.components.WebxdcView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.connect.DcHelper; @@ -62,6 +66,7 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; +import org.thoughtcrime.securesms.mms.VcardSlide; import org.thoughtcrime.securesms.reactions.ReactionsConversationView; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.LongClickMovementMethod; @@ -71,6 +76,7 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; +import java.util.List; import java.util.Locale; import java.util.Set; @@ -113,6 +119,7 @@ public class ConversationItem extends BaseConversationItem private @NonNull Stub documentViewStub; private @NonNull Stub webxdcViewStub; private Stub stickerStub; + private Stub vcardViewStub; private @Nullable EventListener eventListener; private int measureCalls; @@ -146,6 +153,7 @@ public class ConversationItem extends BaseConversationItem this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); this.webxdcViewStub = new Stub<>(findViewById(R.id.webxdc_view_stub)); this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub)); + this.vcardViewStub = new Stub<>(findViewById(R.id.vcard_view_stub)); this.groupSenderHolder = findViewById(R.id.group_sender_holder); this.quoteView = findViewById(R.id.quote_view); this.container = findViewById(R.id.container); @@ -301,6 +309,11 @@ public class ConversationItem extends BaseConversationItem webxdcViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); webxdcViewStub.get().setClickable(batchSelected.isEmpty()); } + + if (vcardViewStub.resolved()) { + vcardViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + vcardViewStub.get().setClickable(batchSelected.isEmpty()); + } } private void setContentDescription() { @@ -315,6 +328,8 @@ public class ConversationItem extends BaseConversationItem desc += documentViewStub.get().getDescription() + "\n"; } else if (webxdcViewStub.resolved() && webxdcViewStub.get().getVisibility() == View.VISIBLE) { desc += webxdcViewStub.get().getDescription() + "\n"; + } else if (vcardViewStub.resolved() && vcardViewStub.get().getVisibility() == View.VISIBLE) { + desc += vcardViewStub.get().getDescription() + "\n"; } else if (mediaThumbnailStub.resolved() && mediaThumbnailStub.get().getVisibility() == View.VISIBLE) { desc += mediaThumbnailStub.get().getDescription() + "\n"; } else if (stickerStub.resolved() && stickerStub.get().getVisibility() == View.VISIBLE) { @@ -362,6 +377,10 @@ public class ConversationItem extends BaseConversationItem return dcMsg.getType()==DcMsg.DC_MSG_WEBXDC; } + private boolean hasVcard(DcMsg dcMsg) { + return dcMsg.getType()==DcMsg.DC_MSG_VCARD; + } + private boolean hasDocument(DcMsg dcMsg) { return dcMsg.getType()==DcMsg.DC_MSG_FILE && !dcMsg.isSetupMessage(); } @@ -463,6 +482,7 @@ public class ConversationItem extends BaseConversationItem if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); //noinspection ConstantConditions int duration = messageRecord.getDuration(); @@ -489,6 +509,7 @@ public class ConversationItem extends BaseConversationItem if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); //noinspection ConstantConditions documentViewStub.get().setDocument(new DocumentSlide(context, messageRecord)); @@ -508,6 +529,7 @@ public class ConversationItem extends BaseConversationItem if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); webxdcViewStub.get().setWebxdc(messageRecord, context.getString(R.string.webxdc_app)); webxdcViewStub.get().setWebxdcClickListener(new ThumbnailClickListener()); @@ -520,12 +542,33 @@ public class ConversationItem extends BaseConversationItem ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); footer.setVisibility(VISIBLE); } + else if (hasVcard(messageRecord)) { + vcardViewStub.get().setVisibility(View.VISIBLE); + if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); + if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); + if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); + if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); + if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); + + vcardViewStub.get().setVcard(glideRequests, new VcardSlide(context, messageRecord), rpc); + vcardViewStub.get().setVcardClickListener(new ThumbnailClickListener()); + vcardViewStub.get().setOnLongClickListener(passthroughClickListener); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + vcardViewStub.get().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + + ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + footer.setVisibility(VISIBLE); + } else if (hasThumbnail(messageRecord)) { mediaThumbnailStub.get().setVisibility(View.VISIBLE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); Slide slide = MediaUtil.getSlideForMsg(context, messageRecord); @@ -566,6 +609,7 @@ public class ConversationItem extends BaseConversationItem if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); bodyBubble.setBackgroundColor(Color.TRANSPARENT); @@ -587,6 +631,7 @@ public class ConversationItem extends BaseConversationItem if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); if (webxdcViewStub.resolved()) webxdcViewStub.get().setVisibility(View.GONE); + if (vcardViewStub.resolved()) vcardViewStub.get().setVisibility(View.GONE); ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -839,6 +884,7 @@ public class ConversationItem extends BaseConversationItem else if (audioViewStub.resolved()) audioViewStub.get().togglePlay(); else if (documentViewStub.resolved()) documentViewStub.get().performClick(); else if (webxdcViewStub.resolved()) webxdcViewStub.get().performClick(); + else if (vcardViewStub.resolved()) vcardViewStub.get().performClick(); } /// Event handlers @@ -847,8 +893,37 @@ public class ConversationItem extends BaseConversationItem public void onClick(final View v, final Slide slide) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { performClick(); - } else if (messageRecord.getType() == DcMsg.DC_MSG_WEBXDC) { + } else if (slide.isWebxdcDocument()) { WebxdcActivity.openWebxdcActivity(context, messageRecord); + } else if (slide.isVcard()) { + try { + String path = slide.asAttachment().getRealPath(context); + VcardContact vcardContact = rpc.parseVcard(path).get(0); + new AlertDialog.Builder(context) + .setMessage(context.getString(R.string.ask_start_chat_with, vcardContact.getDisplayName())) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + try { + List contactIds = rpc.importVcard(dcContext.getAccountId(), path); + if (contactIds.size() > 0) { + int chatId = dcContext.createChatByContactId(contactIds.get(0)); + if (chatId != 0) { + Intent intent = new Intent(context, ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId); + context.startActivity(intent); + return; + } + } + } catch (RpcException e) { + Log.e(TAG, "failed to import vCard", e); + } + Toast.makeText(context, context.getResources().getString(R.string.error), Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } catch (RpcException e) { + Log.e(TAG, "failed to parse vCard", e); + Toast.makeText(context, context.getResources().getString(R.string.error), Toast.LENGTH_SHORT).show(); + } } else if (MediaPreviewActivity.isTypeSupported(slide) && slide.getUri() != null) { Intent intent = new Intent(context, MediaPreviewActivity.class); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java index 593aface7..daec88d9c 100644 --- a/src/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java @@ -1,9 +1,21 @@ package org.thoughtcrime.securesms.attachments; +import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; + +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + public abstract class Attachment { @NonNull @@ -88,4 +100,35 @@ public abstract class Attachment { public int getHeight() { return height; } + + public String getRealPath(Context context) { + try { + // get file in the blobdir as `/[-].` + String filename = getFileName(); + String ext = ""; + if(filename==null) { + filename = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date()); + ext = "." + MediaUtil.getExtensionFromMimeType(getContentType()); + } + else { + int i = filename.lastIndexOf("."); + if(i>=0) { + ext = filename.substring(i); + filename = filename.substring(0, i); + } + } + String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext); + + // copy content to this file + InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri()); + OutputStream outputStream = new FileOutputStream(path); + Util.copy(inputStream, outputStream); + + return path; + } + catch(Exception e) { + e.printStackTrace(); + return null; + } + } } diff --git a/src/org/thoughtcrime/securesms/attachments/DcAttachment.java b/src/org/thoughtcrime/securesms/attachments/DcAttachment.java index 779f127d7..ce5164ad4 100644 --- a/src/org/thoughtcrime/securesms/attachments/DcAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/DcAttachment.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.attachments; import java.io.File; + +import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; @@ -35,4 +37,9 @@ public class DcAttachment extends Attachment { } return getDataUri(); } + + @Override + public String getRealPath(Context context) { + return dcMsg.getFile(); + } } diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index fa9d4f256..7ffdbf6cc 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -20,11 +21,14 @@ import androidx.annotation.RequiresApi; import com.annimon.stream.Stream; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcMsg; +import com.b44t.messenger.rpc.RpcException; +import com.b44t.messenger.rpc.VcardContact; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.json.JSONObject; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; @@ -185,7 +189,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { } private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { - List thumbnailSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker() || s.isWebxdcDocument()).limit(1).toList(); + List thumbnailSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker() || s.isWebxdcDocument() || s.isVcard()).limit(1).toList(); List audioSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasAudio()).limit(1).toList(); List documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList(); @@ -206,7 +210,22 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .into(thumbnailView); } catch (Exception e) { - e.printStackTrace(); + Log.e(TAG, "failed to get webxdc icon", e); + thumbnailView.setVisibility(GONE); + } + } else if (thumbnailSlides.get(0).isVcard()) { + try { + VcardContact vcardContact = DcHelper.getRpc(getContext()).parseVcard(quotedMsg.getFile()).get(0); + Recipient recipient = new Recipient(getContext(), vcardContact); + glideRequests.load(recipient.getContactPhoto(getContext())) + .error(recipient.getFallbackAvatarDrawable(getContext(), false)) + .centerCrop() + .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(thumbnailView); + } catch (RpcException e) { + Log.e(TAG, "failed to parse vCard", e); + thumbnailView.setVisibility(GONE); } } else { Uri thumbnailUri = thumbnailSlides.get(0).getUri(); diff --git a/src/org/thoughtcrime/securesms/components/VcardView.java b/src/org/thoughtcrime/securesms/components/VcardView.java new file mode 100644 index 000000000..36b8e84b5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/VcardView.java @@ -0,0 +1,75 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.b44t.messenger.rpc.Rpc; +import com.b44t.messenger.rpc.RpcException; +import com.b44t.messenger.rpc.VcardContact; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.SlideClickListener; +import org.thoughtcrime.securesms.mms.VcardSlide; +import org.thoughtcrime.securesms.recipients.Recipient; + +public class VcardView extends FrameLayout { + private static final String TAG = VcardView.class.getSimpleName(); + + private final @NonNull AvatarView avatar; + private final @NonNull TextView name; + private final @NonNull TextView address; + + private @Nullable SlideClickListener viewListener; + private @Nullable VcardSlide slide; + + public VcardView(Context context) { + this(context, null); + } + + public VcardView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public VcardView(final Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + inflate(context, R.layout.vcard_view, this); + + this.avatar = findViewById(R.id.avatar); + this.name = findViewById(R.id.name); + this.address = findViewById(R.id.addr); + + setOnClickListener(v -> { + if (viewListener != null && slide != null) { + viewListener.onClick(v, slide); + } + }); + } + + public void setVcardClickListener(@Nullable SlideClickListener listener) { + this.viewListener = listener; + } + + public void setVcard(@NonNull GlideRequests glideRequests, final @NonNull VcardSlide slide, final @NonNull Rpc rpc) { + try { + VcardContact vcardContact = rpc.parseVcard(slide.asAttachment().getRealPath(getContext())).get(0); + name.setText(vcardContact.getDisplayName()); + address.setText(vcardContact.getAddr()); + avatar.setAvatar(glideRequests, new Recipient(getContext(), vcardContact), false); + this.slide = slide; + } catch (RpcException e) { + Log.e(TAG, "failed to parse vCard", e); + } + } + + public String getDescription() { + return name.getText() + "\n" + address.getText(); + } +} diff --git a/src/org/thoughtcrime/securesms/connect/DcHelper.java b/src/org/thoughtcrime/securesms/connect/DcHelper.java index 91dc1738f..09a0ae27d 100644 --- a/src/org/thoughtcrime/securesms/connect/DcHelper.java +++ b/src/org/thoughtcrime/securesms/connect/DcHelper.java @@ -260,6 +260,7 @@ public class DcHelper { dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other)); dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait)); dcContext.setStockTranslation(191, context.getString(R.string.secure_join_wait_timeout)); + dcContext.setStockTranslation(200, context.getString(R.string.contact)); } public static File getImexDir() { diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java index d9e50f40f..07888ff3f 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java @@ -24,17 +24,21 @@ public class GeneratedContactPhoto implements FallbackContactPhoto { @Override public Drawable asDrawable(Context context, int color) { + return asDrawable(context, color, true); + } + + public Drawable asDrawable(Context context, int color, boolean roundShape) { int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - return TextDrawable.builder() - .beginConfig() - .width(targetSize) - .height(targetSize) - .textColor(Color.WHITE) - .bold() - .toUpperCase() - .endConfig() - .buildRound(getCharacter(name), color); + TextDrawable.IShapeBuilder builder = TextDrawable.builder() + .beginConfig() + .width(targetSize) + .height(targetSize) + .textColor(Color.WHITE) + .bold() + .toUpperCase() + .endConfig(); + return roundShape? builder.buildRound(getCharacter(name), color) : builder.buildRect(getCharacter(name), color); } private String getCharacter(String name) { diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java new file mode 100644 index 000000000..36fac9f10 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.contacts.avatars; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.b44t.messenger.rpc.VcardContact; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +public class VcardContactPhoto implements ContactPhoto { + private final VcardContact vContact; + + public VcardContactPhoto(VcardContact vContact) { + this.vContact = vContact; + } + + @Override + public InputStream openInputStream(Context context) throws IOException { + byte[] blob = vContact.getProfileImage(); + return (blob == null)? null : new ByteArrayInputStream(blob); + } + + @Override + public @Nullable Uri getUri(@NonNull Context context) { + return null; + } + + @Override + public boolean isProfilePhoto() { + return true; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(vContact.getAddr().getBytes()); + messageDigest.update(ByteBuffer.allocate(4).putFloat(vContact.getTimestamp()).array()); + } +} diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 135d67823..eedc1dcff 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -41,12 +41,12 @@ import androidx.appcompat.app.AlertDialog; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; +import com.b44t.messenger.rpc.RpcException; import com.b44t.messenger.util.concurrent.ListenableFuture; import com.b44t.messenger.util.concurrent.ListenableFuture.Listener; import com.b44t.messenger.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.ShareLocationDialog; @@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.components.VcardView; import org.thoughtcrime.securesms.components.WebxdcView; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -93,6 +94,7 @@ public class AttachmentManager { private AudioView audioView; private DocumentView documentView; private WebxdcView webxdcView; + private VcardView vcardView; //private SignalMapView mapView; private final @NonNull List garbage = new LinkedList<>(); @@ -116,6 +118,7 @@ public class AttachmentManager { this.audioView = ViewUtil.findById(root, R.id.attachment_audio); this.documentView = ViewUtil.findById(root, R.id.attachment_document); this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc); + this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard); //this.mapView = ViewUtil.findById(root, R.id.attachment_location); this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); @@ -126,6 +129,7 @@ public class AttachmentManager { audioView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); documentView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); webxdcView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); + vcardView.getBackground().setColorFilter(incomingBubbleColor, PorterDuff.Mode.MULTIPLY); } } @@ -308,6 +312,9 @@ public class AttachmentManager { audioView.setAudio((AudioSlide) slide, 0); removableMediaView.display(audioView, false); result.set(true); + } else if (slide.isVcard()) { + vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context)); + removableMediaView.display(vcardView, false); } else if (slide.hasDocument()) { if (slide.isWebxdcDocument()) { DcMsg instance = msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId); @@ -685,13 +692,25 @@ public class AttachmentManager { DcContext dcContext = DcHelper.getContext(context); DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC); Attachment attachment = new UriAttachment(uri, null, MediaUtil.WEBXDC, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, fileName, null, false); - String path = ConversationActivity.getRealPathFromAttachment(context, attachment); + String path = attachment.getRealPath(context); msg.setFile(path, MediaUtil.WEBXDC); dcContext.setDraft(chatId, msg); return new DocumentSlide(context, msg); - } else { - return new DocumentSlide(context, uri, mimeType, dataSize, fileName); } + + if (mimeType.equals(MediaUtil.VCARD) || (fileName != null && (fileName.endsWith(".vcf") || fileName.endsWith(".vcard")))) { + VcardSlide slide = new VcardSlide(context, uri, dataSize, fileName); + String path = slide.asAttachment().getRealPath(context); + try { + if (DcHelper.getRpc(context).parseVcard(path).size() == 1) { + return slide; + } + } catch (RpcException e) { + e.printStackTrace(); + } + } + + return new DocumentSlide(context, uri, mimeType, dataSize, fileName); default: throw new AssertionError("unrecognized enum"); } } diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 89d40e9ef..56fd0d842 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -98,6 +98,10 @@ public abstract class Slide { return false; } + public boolean isVcard() { + return false; + } + public boolean hasLocation() { return false; } diff --git a/src/org/thoughtcrime/securesms/mms/VcardSlide.java b/src/org/thoughtcrime/securesms/mms/VcardSlide.java new file mode 100644 index 000000000..01f1d7625 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/VcardSlide.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.mms; + + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.b44t.messenger.DcMsg; + +public class VcardSlide extends DocumentSlide { + + public VcardSlide(Context context, DcMsg dcMsg) { + super(context, dcMsg); + } + + public VcardSlide(@NonNull Context context, @NonNull Uri uri, long size, @Nullable String fileName) { + super(context, uri, "text/vcard", size, fileName); + } + + @Override + public boolean isVcard() { + return true; + } +} diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 9225e79dd..3942c3bd6 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -21,13 +21,15 @@ import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; +import com.b44t.messenger.rpc.VcardContact; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; @@ -37,15 +39,14 @@ import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.LocalFileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto; +import org.thoughtcrime.securesms.contacts.avatars.VcardContactPhoto; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.util.Hash; import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.WeakHashMap; @@ -65,6 +66,7 @@ public class Recipient { private final @Nullable DcChat dcChat; private @Nullable DcContact dcContact; + private final @Nullable VcardContact vContact; public static @NonNull Recipient fromChat(@NonNull Context context, int dcMsgId) { DcContext dcContext = DcHelper.getContext(context); @@ -90,21 +92,26 @@ public class Recipient { } public Recipient(@NonNull Context context, @NonNull DcChat dcChat) { - this(context, dcChat, null, null); + this(context, dcChat, null, null, null); + } + + public Recipient(@NonNull Context context, @NonNull VcardContact vContact) { + this(context, null, null, null, vContact); } public Recipient(@NonNull Context context, @NonNull DcContact dcContact) { - this(context, null, dcContact, null); + this(context, null, dcContact, null, null); } public Recipient(@NonNull Context context, @NonNull DcContact dcContact, @NonNull String profileName) { - this(context, null, dcContact, profileName); + this(context, null, dcContact, profileName, null); } - private Recipient(@NonNull Context context, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable String profileName) { + private Recipient(@NonNull Context context, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable String profileName, @Nullable VcardContact vContact) { this.dcChat = dcChat; this.dcContact = dcContact; this.profileName = profileName; + this.vContact = vContact; this.contactUri = null; this.systemContactPhoto = null; this.customLabel = null; @@ -141,6 +148,9 @@ public class Recipient { else if(dcContact!=null) { return dcContact.getDisplayName(); } + else if(vContact!=null) { + return vContact.getDisplayName(); + } return ""; } @@ -168,18 +178,6 @@ public class Recipient { return dcChat!=null && dcChat.isMultiUser(); } - public @NonNull List loadParticipants(Context context) { - List participants = new ArrayList<>(); - if (dcChat!=null) { - DcContext dcContext = DcHelper.getAccounts(context).getAccount(dcChat.getAccountId()); - int[] contactIds = dcContext.getChatContacts(dcChat.getId()); - for (int contactId : contactIds) { - participants.add(new Recipient(context, dcContext.getContact(contactId))); - } - } - return participants; - } - public synchronized void addListener(RecipientModifiedListener listener) { listeners.add(listener); } @@ -200,15 +198,21 @@ public class Recipient { else if(dcContact!=null) { rgb = dcContact.getColor(); } - int argb = Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb)); - return argb; + else if(vContact!=null) { + rgb = Color.parseColor(vContact.getColor()); + } + return Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb)); } public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context) { - return getFallbackContactPhoto().asDrawable(context, getFallbackAvatarColor()); + return getFallbackAvatarDrawable(context, true); } - public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() { + public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context, boolean roundShape) { + return getFallbackContactPhoto().asDrawable(context, getFallbackAvatarColor(), roundShape); + } + + public synchronized @NonNull GeneratedContactPhoto getFallbackContactPhoto() { String name = getName(); if (!TextUtils.isEmpty(profileName)) return new GeneratedContactPhoto(profileName); else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name); @@ -231,6 +235,10 @@ public class Recipient { } } + if (vContact!=null && vContact.hasProfileImage()) { + return new VcardContactPhoto(vContact); + } + if (systemContactPhoto != null) { return new SystemContactPhoto(address, systemContactPhoto, 0); } @@ -262,7 +270,7 @@ public class Recipient { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof Recipient)) return false; + if (!(o instanceof Recipient)) return false; Recipient that = (Recipient) o; @@ -290,6 +298,7 @@ public class Recipient { return dcChat!=null? dcChat : new DcChat(0, 0); } + @NonNull @Override public String toString() { return "Recipient{" + diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index 70ca04ce4..7b205b992 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.StickerSlide; +import org.thoughtcrime.securesms.mms.VcardSlide; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; @@ -48,6 +49,7 @@ public class MediaUtil { public static final String VIDEO_UNSPECIFIED = "video/*"; public static final String OCTET = "application/octet-stream"; public static final String WEBXDC = "application/webxdc+zip"; + public static final String VCARD = "text/vcard"; public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { @@ -63,6 +65,8 @@ public class MediaUtil { } else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO || dcMsg.getType() == DcMsg.DC_MSG_VOICE) { slide = new AudioSlide(context, dcMsg); + } else if (dcMsg.getType() == DcMsg.DC_MSG_VCARD) { + slide = new VcardSlide(context, dcMsg); } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE || dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) { slide = new DocumentSlide(context, dcMsg); @@ -301,6 +305,8 @@ public class MediaUtil { return "webp"; case WEBXDC: return "xdc"; + case VCARD: + return "vcf"; } return null; }