diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index e44d2a2c09b722028dca128d6f1bd40217d40e0a..f266c18e39d227c4f3421118b380ba70c44d5605 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -57,6 +57,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final int TYPE_STATUS = 3; public static final int TYPE_PRIVATE = 4; public static final int TYPE_PRIVATE_FILE = 5; + public static final int TYPE_RTP_SESSION = 6; public static final String CONVERSATION = "conversationUuid"; public static final String COUNTERPART = "counterpart"; @@ -151,6 +152,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null); } + public Message(Conversation conversation, int status, int type, final String remoteMsgId) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + null, + System.currentTimeMillis(), + Message.ENCRYPTION_NONE, + status, + type, + false, + remoteMsgId, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, final int encryption, final int status, final int type, final boolean carbon, diff --git a/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java b/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..8e360cb27389092a55d2ac78e5b119f2a1120ef4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java @@ -0,0 +1,59 @@ +package eu.siacs.conversations.entities; + +import android.support.annotation.DrawableRes; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.R; + +public class RtpSessionStatus { + + public final boolean successful; + public final long duration; + + + public RtpSessionStatus(boolean successful, long duration) { + this.successful = successful; + this.duration = duration; + } + + @Override + public String toString() { + return successful + ":" + duration; + } + + public static RtpSessionStatus of(final String body) { + final String[] parts = Strings.nullToEmpty(body).split(":", 2); + long duration = 0; + if (parts.length == 2) { + try { + duration = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + //do nothing + } + } + boolean made; + try { + made = Boolean.parseBoolean(parts[0]); + } catch (Exception e) { + made = false; + } + return new RtpSessionStatus(made, duration); + } + + public static @DrawableRes int getDrawable(final boolean received, final boolean successful, final boolean darkTheme) { + if (received) { + if (successful) { + return darkTheme ? R.drawable.ic_call_received_white_18dp : R.drawable.ic_call_received_black_18dp; + } else { + return darkTheme ? R.drawable.ic_call_missed_white_18dp : R.drawable.ic_call_missed_black_18dp; + } + } else { + if (successful) { + return darkTheme ? R.drawable.ic_call_made_white_18dp : R.drawable.ic_call_made_black_18dp; + } else { + return darkTheme ? R.drawable.ic_call_missed_outgoing_white_18dp : R.drawable.ic_call_missed_outgoing_black_18dp; + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 3ee927e002eea12dec9ac6bcaa10d75e6d692853..a96d67b345e8673d0c62aad42083b15ca4c01ddd 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -49,6 +49,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.persistance.FileBackend; @@ -72,926 +73,959 @@ import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.StylingHelper; +import eu.siacs.conversations.utils.TimeframeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.mam.MamReference; import rocks.xmpp.addr.Jid; public class MessageAdapter extends ArrayAdapter implements CopyTextView.CopyHandler { - public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; - private static final int SENT = 0; - private static final int RECEIVED = 1; - private static final int STATUS = 2; - private static final int DATE_SEPARATOR = 3; - private final XmppActivity activity; - private final ListSelectionManager listSelectionManager = new ListSelectionManager(); - private final AudioPlayer audioPlayer; - private List highlightedTerm = null; - private DisplayMetrics metrics; - private OnContactPictureClicked mOnContactPictureClickedListener; - private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private boolean mUseGreenBackground = false; - private OnQuoteListener onQuoteListener; - public MessageAdapter(XmppActivity activity, List messages) { - super(activity, 0, messages); - this.audioPlayer = new AudioPlayer(this); - this.activity = activity; - metrics = getContext().getResources().getDisplayMetrics(); - updatePreferences(); - } - - - - private static void resetClickListener(View... views) { - for (View view : views) { - view.setOnClickListener(null); - } - } - - public void flagScreenOn() { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public void flagScreenOff() { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public void setOnContactPictureClicked(OnContactPictureClicked listener) { - this.mOnContactPictureClickedListener = listener; - } - - public Activity getActivity() { - return activity; - } - - public void setOnContactPictureLongClicked( - OnContactPictureLongClicked listener) { - this.mOnContactPictureLongClickedListener = listener; - } - - public void setOnQuoteListener(OnQuoteListener listener) { - this.onQuoteListener = listener; - } - - @Override - public int getViewTypeCount() { - return 4; - } - - private int getItemViewType(Message message) { - if (message.getType() == Message.TYPE_STATUS) { - if (DATE_SEPARATOR_BODY.equals(message.getBody())) { - return DATE_SEPARATOR; - } else { - return STATUS; - } - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; - } - - return SENT; - } - - @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); - } - - private int getMessageTextColor(boolean onDark, boolean primary) { - if (onDark) { - return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70); - } else { - return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54); - } - } - - private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { - String filesize = null; - String info = null; - boolean error = false; - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } - - if (viewHolder.edit_indicator != null) { - if (message.edited()) { - viewHolder.edit_indicator.setVisibility(View.VISIBLE); - viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); - viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); - } else { - viewHolder.edit_indicator.setVisibility(View.GONE); - } - } - final Transferable transferable = message.getTransferable(); - boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getMergedStatus() <= Message.STATUS_RECEIVED; - if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) { - FileParams params = message.getFileParams(); - filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null; - if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { - error = true; - } - } - switch (message.getMergedStatus()) { - case Message.STATUS_WAITING: - info = getContext().getString(R.string.waiting); - break; - case Message.STATUS_UNSEND: - if (transferable != null) { - info = getContext().getString(R.string.sending_file, transferable.getProgress()); - } else { - info = getContext().getString(R.string.sending); - } - break; - case Message.STATUS_OFFERED: - info = getContext().getString(R.string.offering); - break; - case Message.STATUS_SEND_RECEIVED: - case Message.STATUS_SEND_DISPLAYED: - viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp); - viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - break; - case Message.STATUS_SEND_FAILED: - final String errorMessage = message.getErrorMessage(); - if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { - info = getContext().getString(R.string.cancelled); - } else if (errorMessage != null) { - final String[] errorParts = errorMessage.split("\\u001f", 2); - if (errorParts.length == 2) { - switch (errorParts[0]) { - case "file-too-large": - info = getContext().getString(R.string.file_too_large); - break; - default: - info = getContext().getString(R.string.send_failed); - break; - } - } else { - info = getContext().getString(R.string.send_failed); - } - } else { - info = getContext().getString(R.string.send_failed); - } - error = true; - break; - default: - if (multiReceived) { - info = UIHelper.getMessageDisplayName(message); - } - break; - } - if (error && type == SENT) { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning); - } - } else { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption); - } - viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false)); - } - if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator.setVisibility(View.GONE); - } else { - boolean verified = false; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = message.getConversation() - .getAccount().getAxolotlService().getFingerprintTrust( - message.getFingerprint()); - if (status != null && status.isVerified()) { - verified = true; - } - } - if (verified) { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp); - } else { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); - } - if (darkBackground) { - viewHolder.indicator.setAlpha(0.7f); - } else { - viewHolder.indicator.setAlpha(0.57f); - } - viewHolder.indicator.setVisibility(View.VISIBLE); - } - - final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); - final String bodyLanguage = message.getBodyLanguage(); - final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); - if (message.getStatus() <= Message.STATUS_RECEIVED) { ; - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime+bodyLanguageInfo); - } - } else { - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - if (error) { - viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(info); - } - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime+bodyLanguageInfo); - } - } - } - - private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary); - } - viewHolder.messageBody.setTextIsSelectable(false); - } - - private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji); - } - Spannable span = new SpannableString(body); - float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; - span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(EmojiWrapper.transform(span)); - } - - private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { - if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { - body.insert(start++, "\n"); - body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - end++; - } - if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { - body.insert(end, "\n"); - body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - int color = darkBackground ? this.getMessageTextColor(darkBackground, false) - : ContextCompat.getColor(activity, R.color.green700_desaturated); - DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - /** - * Applies QuoteSpan to group of lines which starts with > or » characters. - * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. - */ - private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { - boolean startsWithQuote = false; - char previous = '\n'; - int lineStart = -1; - int lineTextStart = -1; - int quoteStart = -1; - for (int i = 0; i <= body.length(); i++) { - char current = body.length() > i ? body.charAt(i) : '\n'; - if (lineStart == -1) { - if (previous == '\n') { - if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) - || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { - // Line start with quote - lineStart = i; - if (quoteStart == -1) quoteStart = i; - if (i == 0) startsWithQuote = true; - } else if (quoteStart >= 0) { - // Line start without quote, apply spans there - applyQuoteSpan(body, quoteStart, i - 1, darkBackground); - quoteStart = -1; - } - } - } else { - // Remove extra spaces between > and first character in the line - // > character will be removed too - if (current != ' ' && lineTextStart == -1) { - lineTextStart = i; - } - if (current == '\n') { - body.delete(lineStart, lineTextStart); - i -= lineTextStart - lineStart; - if (i == lineStart) { - // Avoid empty lines because span over empty line can be hidden - body.insert(i++, " "); - } - lineStart = -1; - lineTextStart = -1; - } - } - previous = current; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(body, quoteStart, body.length(), darkBackground); - } - return startsWithQuote; - } - - private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); - } - viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground - ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500)); - viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); - - if (message.getBody() != null) { - final String nick = UIHelper.getMessageDisplayName(message); - SpannableStringBuilder body = message.getMergedBody(); - boolean hasMeCommand = message.hasMeCommand(); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); - } - if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { - body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); - body.append("\u2026"); - } - Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); - for (Message.MergeSeparator mergeSeparator : mergeSeparators) { - int start = body.getSpanStart(mergeSeparator); - int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - boolean startsWithQuote = handleTextQuotes(body, darkBackground); - if (!message.isPrivateMessage()) { - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } else { - String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - body.insert(0, privateMarker); - int privateMarkerIndex = privateMarker.length(); - if (startsWithQuote) { - body.insert(privateMarkerIndex, "\n\n"); - body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - body.insert(privateMarkerIndex, " "); - } - body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1, - privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation() instanceof Conversation) { - final Conversation conversation = (Conversation) message.getConversation(); - Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); - Matcher matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - } - Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); - while (matcher.find()) { - if (matcher.start() < matcher.end()) { - body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); - if (highlightedTerm != null) { - StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); - } - MyLinkify.addLinks(body,true); - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(EmojiWrapper.transform(body)); - viewHolder.messageBody.setTextIsSelectable(true); - viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - listSelectionManager.onUpdate(viewHolder.messageBody, message); - } else { - viewHolder.messageBody.setText(""); - viewHolder.messageBody.setTextIsSelectable(false); - } - } - - private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(text); - viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); - } - - private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); - } - - private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - viewHolder.download_button.setOnClickListener(v -> showLocation(message)); - } - - private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.GONE); - final RelativeLayout audioPlayer = viewHolder.audioPlayer; - audioPlayer.setVisibility(View.VISIBLE); - AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground); - this.audioPlayer.init(audioPlayer, message); - } - - private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.VISIBLE); - final FileParams params = message.getFileParams(); - final double target = metrics.density * 288; - final int scaledW; - final int scaledH; - if (Math.max(params.height, params.width) * metrics.density <= target) { - scaledW = (int) (params.width * metrics.density); - scaledH = (int) (params.height * metrics.density); - } else if (Math.max(params.height, params.width) <= target) { - scaledW = params.width; - scaledH = params.height; - } else if (params.width <= params.height) { - scaledW = (int) (params.width / ((double) params.height / target)); - scaledH = (int) target; - } else { - scaledW = (int) target; - scaledH = (int) (params.height / ((double) params.width / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.image.setLayoutParams(layoutParams); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> openDownloadable(message)); - } - - private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - if (message.isPrivateMessage()) { - final String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - final SpannableString body = new SpannableString(privateMarker); - body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setVisibility(View.VISIBLE); - } else { - viewHolder.messageBody.setVisibility(View.GONE); - } - } - - private void loadMoreMessages(Conversation conversation) { - conversation.setLastClearHistory(0, null); - activity.xmppConnectionService.updateConversation(conversation); - conversation.setHasMessagesLeftOnServer(true); - conversation.setFirstMamReference(null); - long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - conversation.messagesLoaded.set(true); - MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public View getView(int position, View view, ViewGroup parent) { - final Message message = getItem(position); - final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; - final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); - final Conversational conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final int type = getItemViewType(position); - ViewHolder viewHolder; - if (view == null) { - viewHolder = new ViewHolder(); - switch (type) { - case DATE_SEPARATOR: - view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); + public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int DATE_SEPARATOR = 3; + private static final int RTP_SESSION = 4; + private final XmppActivity activity; + private final ListSelectionManager listSelectionManager = new ListSelectionManager(); + private final AudioPlayer audioPlayer; + private List highlightedTerm = null; + private DisplayMetrics metrics; + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + private boolean mUseGreenBackground = false; + private OnQuoteListener onQuoteListener; + + public MessageAdapter(XmppActivity activity, List messages) { + super(activity, 0, messages); + this.audioPlayer = new AudioPlayer(this); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + updatePreferences(); + } + + + private static void resetClickListener(View... views) { + for (View view : views) { + view.setOnClickListener(null); + } + } + + public void flagScreenOn() { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void flagScreenOff() { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public Activity getActivity() { + return activity; + } + + public void setOnContactPictureLongClicked( + OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + public void setOnQuoteListener(OnQuoteListener listener) { + this.onQuoteListener = listener; + } + + @Override + public int getViewTypeCount() { + return 5; + } + + private int getItemViewType(Message message) { + if (message.getType() == Message.TYPE_STATUS) { + if (DATE_SEPARATOR_BODY.equals(message.getBody())) { + return DATE_SEPARATOR; + } else { + return STATUS; + } + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return RTP_SESSION; + } else if (message.getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + @Override + public int getItemViewType(int position) { + return this.getItemViewType(getItem(position)); + } + + private int getMessageTextColor(boolean onDark, boolean primary) { + if (onDark) { + return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70); + } else { + return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54); + } + } + + private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { + String filesize = null; + String info = null; + boolean error = false; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + + if (viewHolder.edit_indicator != null) { + if (message.edited()) { + viewHolder.edit_indicator.setVisibility(View.VISIBLE); + viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); + viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); + } else { + viewHolder.edit_indicator.setVisibility(View.GONE); + } + } + final Transferable transferable = message.getTransferable(); + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getMergedStatus() <= Message.STATUS_RECEIVED; + if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) { + FileParams params = message.getFileParams(); + filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null; + if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { + error = true; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + if (transferable != null) { + info = getContext().getString(R.string.sending_file, transferable.getProgress()); + } else { + info = getContext().getString(R.string.sending); + } + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + case Message.STATUS_SEND_DISPLAYED: + viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp); + viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + break; + case Message.STATUS_SEND_FAILED: + final String errorMessage = message.getErrorMessage(); + if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { + info = getContext().getString(R.string.cancelled); + } else if (errorMessage != null) { + final String[] errorParts = errorMessage.split("\\u001f", 2); + if (errorParts.length == 2) { + switch (errorParts[0]) { + case "file-too-large": + info = getContext().getString(R.string.file_too_large); + break; + default: + info = getContext().getString(R.string.send_failed); + break; + } + } else { + info = getContext().getString(R.string.send_failed); + } + } else { + info = getContext().getString(R.string.send_failed); + } + error = true; + break; + default: + if (multiReceived) { + info = UIHelper.getMessageDisplayName(message); + } + break; + } + if (error && type == SENT) { + if (darkBackground) { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark); + } else { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning); + } + } else { + if (darkBackground) { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark); + } else { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption); + } + viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false)); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + boolean verified = false; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + final FingerprintStatus status = message.getConversation() + .getAccount().getAxolotlService().getFingerprintTrust( + message.getFingerprint()); + if (status != null && status.isVerified()) { + verified = true; + } + } + if (verified) { + viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp); + } else { + viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); + } + if (darkBackground) { + viewHolder.indicator.setAlpha(0.7f); + } else { + viewHolder.indicator.setAlpha(0.57f); + } + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + final String bodyLanguage = message.getBodyLanguage(); + final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize == null) && (info != null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); + } else { + viewHolder.time.setText(formattedTime + bodyLanguageInfo); + } + } else { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize == null) && (info != null)) { + if (error) { + viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); + } else { + viewHolder.time.setText(info); + } + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); + } else { + viewHolder.time.setText(formattedTime + bodyLanguageInfo); + } + } + } + + private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(text); + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary); + } + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji); + } + Spannable span = new SpannableString(body); + float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; + span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(EmojiWrapper.transform(span)); + } + + private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { + if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + int color = darkBackground ? this.getMessageTextColor(darkBackground, false) + : ContextCompat.getColor(activity, R.color.green700_desaturated); + DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + /** + * Applies QuoteSpan to group of lines which starts with > or » characters. + * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. + */ + private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { + boolean startsWithQuote = false; + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) + || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + return startsWithQuote; + } + + private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); + } + viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground + ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500)); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + + if (message.getBody() != null) { + final String nick = UIHelper.getMessageDisplayName(message); + SpannableStringBuilder body = message.getMergedBody(); + boolean hasMeCommand = message.hasMeCommand(); + if (hasMeCommand) { + body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); + } + if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("\u2026"); + } + Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); + for (Message.MergeSeparator mergeSeparator : mergeSeparators) { + int start = body.getSpanStart(mergeSeparator); + int end = body.getSpanEnd(mergeSeparator); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + boolean startsWithQuote = handleTextQuotes(body, darkBackground); + if (!message.isPrivateMessage()) { + if (hasMeCommand) { + body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + body.insert(0, privateMarker); + int privateMarkerIndex = privateMarker.length(); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } + body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hasMeCommand) { + body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1, + privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation() instanceof Conversation) { + final Conversation conversation = (Conversation) message.getConversation(); + Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); + while (matcher.find()) { + if (matcher.start() < matcher.end()) { + body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); + if (highlightedTerm != null) { + StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); + } + MyLinkify.addLinks(body, true); + viewHolder.messageBody.setAutoLinkMask(0); + viewHolder.messageBody.setText(EmojiWrapper.transform(body)); + viewHolder.messageBody.setTextIsSelectable(true); + viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); + listSelectionManager.onUpdate(viewHolder.messageBody, message); + } else { + viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); + } + } + + private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(text); + viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); + } + + private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); + viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); + } + + private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + viewHolder.download_button.setOnClickListener(v -> showLocation(message)); + } + + private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.GONE); + final RelativeLayout audioPlayer = viewHolder.audioPlayer; + audioPlayer.setVisibility(View.VISIBLE); + AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground); + this.audioPlayer.init(audioPlayer, message); + } + + private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + final FileParams params = message.getFileParams(); + final double target = metrics.density * 288; + final int scaledW; + final int scaledH; + if (Math.max(params.height, params.width) * metrics.density <= target) { + scaledW = (int) (params.width * metrics.density); + scaledH = (int) (params.height * metrics.density); + } else if (Math.max(params.height, params.width) <= target) { + scaledW = params.width; + scaledH = params.height; + } else if (params.width <= params.height) { + scaledW = (int) (params.width / ((double) params.height / target)); + scaledH = (int) target; + } else { + scaledW = (int) target; + scaledH = (int) (params.height / ((double) params.width / target)); + } + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); + layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); + viewHolder.image.setLayoutParams(layoutParams); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> openDownloadable(message)); + } + + private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + if (message.isPrivateMessage()) { + final String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + final SpannableString body = new SpannableString(privateMarker); + body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + viewHolder.messageBody.setVisibility(View.VISIBLE); + } else { + viewHolder.messageBody.setVisibility(View.GONE); + } + } + + private void loadMoreMessages(Conversation conversation) { + conversation.setLastClearHistory(0, null); + activity.xmppConnectionService.updateConversation(conversation); + conversation.setHasMessagesLeftOnServer(true); + conversation.setFirstMamReference(null); + long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); + if (timestamp == 0) { + timestamp = System.currentTimeMillis(); + } + conversation.messagesLoaded.set(true); + MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); + if (query != null) { + Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message message = getItem(position); + final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; + final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); + final Conversational conversation = message.getConversation(); + final Account account = conversation.getAccount(); + final int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case DATE_SEPARATOR: + view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + break; + case RTP_SESSION: + view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false); viewHolder.status_message = view.findViewById(R.id.message_body); viewHolder.message_box = view.findViewById(R.id.message_box); - break; - case SENT: - view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - break; - case RECEIVED: - view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.encryption = view.findViewById(R.id.message_encryption); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - break; - case STATUS: - view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.status_message = view.findViewById(R.id.status_message); - viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); break; - default: - throw new AssertionError("Unknown view type"); - } - if (viewHolder.messageBody != null) { - listSelectionManager.onCreate(viewHolder.messageBody, - new MessageBodyActionModeCallback(viewHolder.messageBody)); - viewHolder.messageBody.setCopyHandler(this); - } - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - if (viewHolder == null) { - return view; - } - } - - boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme(); - - if (type == DATE_SEPARATOR) { - if (UIHelper.today(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.today); - } else if (UIHelper.yesterday(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.yesterday); - } else { - viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); - } - viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); - return view; - } else if (type == STATUS) { - if ("LOAD_MORE".equals(message.getBody())) { - viewHolder.status_message.setVisibility(View.GONE); - viewHolder.contact_picture.setVisibility(View.GONE); - viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); - } else { - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setVisibility(View.GONE); - viewHolder.status_message.setText(message.getBody()); - boolean showAvatar; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else { - showAvatar = false; - } - if (showAvatar) { - viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.contact_picture.setVisibility(View.VISIBLE); - } else { - viewHolder.contact_picture.setVisibility(View.GONE); - } - } - return view; - } else { - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); - } - - resetClickListener(viewHolder.message_box, viewHolder.messageBody); - - viewHolder.contact_picture.setOnClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener - .onContactPictureClicked(message); - } - - }); - viewHolder.contact_picture.setOnLongClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(v, message); - return true; - } else { - return false; - } - }); - - final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); - if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { - if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground); - } - } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { - displayMediaPreviewMessage(viewHolder, message, darkBackground); - } else if (message.getFileParams().runtime > 0) { - displayAudioMessage(viewHolder, message, darkBackground); - } else { - displayOpenableMessage(viewHolder, message, darkBackground); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - if (account.isPgpDecryptionServiceConnected()) { - if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { - displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); - } - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground); - viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); - viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { - displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground); - } else { - if (message.isGeoUri()) { - displayLocationMessage(viewHolder, message, darkBackground); - } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { - displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); - } else if (message.treatAsDownloadable()) { - try { - URL url = new URL(message.getBody()); - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } else { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - url.getHost()), - darkBackground); - } - } catch (Exception e) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } - } else { - displayTextMessage(viewHolder, message, darkBackground, type); - } - } - - if (type == RECEIVED) { - if (isInValidSession) { - int bubble; - if (!mUseGreenBackground) { - bubble = activity.getThemeResource(R.attr.message_bubble_received_monochrome, R.drawable.message_bubble_received_white); - } else { - bubble = activity.getThemeResource(R.attr.message_bubble_received_green, R.drawable.message_bubble_received); - } - viewHolder.message_box.setBackgroundResource(bubble); - viewHolder.encryption.setVisibility(View.GONE); - } else { - viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); - viewHolder.encryption.setVisibility(View.VISIBLE); - if (omemoEncryption && !message.isTrusted()) { - viewHolder.encryption.setText(R.string.not_trusted); - } else { - viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); - } - } - } - - displayStatus(viewHolder, message, type, darkBackground); - - return view; - } - - private void promptOpenKeychainInstall(View view) { - activity.showInstallPgpDialog(); - } - - @Override - public void notifyDataSetChanged() { - listSelectionManager.onBeforeNotifyDataSetChanged(); - super.notifyDataSetChanged(); - listSelectionManager.onAfterNotifyDataSetChanged(); - } - - private String transformText(CharSequence text, int start, int end, boolean forCopy) { - SpannableStringBuilder builder = new SpannableStringBuilder(text); - Object copySpan = new Object(); - builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); - for (DividerSpan dividerSpan : dividerSpans) { - builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), - dividerSpan.isLarge() ? "\n\n" : "\n"); - } - start = builder.getSpanStart(copySpan); - end = builder.getSpanEnd(copySpan); - if (start == -1 || end == -1) return ""; - builder = new SpannableStringBuilder(builder, start, end); - if (forCopy) { - QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); - for (QuoteSpan quoteSpan : quoteSpans) { - builder.insert(builder.getSpanStart(quoteSpan), "> "); - } - } - return builder.toString(); - } - - @Override - public String transformTextForCopy(CharSequence text, int start, int end) { - if (text instanceof Spanned) { - return transformText(text, start, end, true); - } else { - return text.toString().substring(start, end); - } - } - - public FileBackend getFileBackend() { - return activity.xmppConnectionService.getFileBackend(); - } - - public void stopAudioPlayer() { - audioPlayer.stop(); - } - - public void unregisterListenerInAudioPlayer() { - audioPlayer.unregisterListener(); - } - - public void startStopPending() { - audioPlayer.startStopPending(); - } - - public void openDownloadable(Message message) { - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ConversationFragment.registerPendingMessage(activity, message); - ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE); - return; - } - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - ViewUtil.view(activity, file); - } - - private void showLocation(Message message) { - for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { - if (intent.resolveActivity(getContext().getPackageManager()) != null) { - getContext().startActivity(intent); - return; - } - } - Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show(); - } - - public void updatePreferences() { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background)); - } - - - public void setHighlightedTerm(List terms) { - this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); - } - - public interface OnQuoteListener { - void onQuote(String text); - } - - public interface OnContactPictureClicked { - void onContactPictureClicked(Message message); - } - - public interface OnContactPictureLongClicked { - void onContactPictureLongClicked(View v, Message message); - } - - private static class ViewHolder { - - public Button load_more_messages; - public ImageView edit_indicator; - public RelativeLayout audioPlayer; - protected LinearLayout message_box; - protected Button download_button; - protected ImageView image; - protected ImageView indicator; - protected ImageView indicatorReceived; - protected TextView time; - protected CopyTextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - } - - - private class MessageBodyActionModeCallback implements ActionMode.Callback { - - private final TextView textView; - - public MessageBodyActionModeCallback(TextView textView) { - this.textView = textView; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - if (onQuoteListener != null) { - int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); - // 3rd item is placed after "copy" item - menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - return false; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (item.getItemId() == android.R.id.button1) { - int start = textView.getSelectionStart(); - int end = textView.getSelectionEnd(); - if (end > start) { - String text = transformText(textView.getText(), start, end, false); - if (onQuoteListener != null) { - onQuoteListener.onQuote(text); - } - mode.finish(); - } - return true; - } - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - } - } + case SENT: + view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + break; + case RECEIVED: + view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.encryption = view.findViewById(R.id.message_encryption); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + break; + case STATUS: + view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.status_message = view.findViewById(R.id.status_message); + viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); + break; + default: + throw new AssertionError("Unknown view type"); + } + if (viewHolder.messageBody != null) { + listSelectionManager.onCreate(viewHolder.messageBody, + new MessageBodyActionModeCallback(viewHolder.messageBody)); + viewHolder.messageBody.setCopyHandler(this); + } + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + if (viewHolder == null) { + return view; + } + } + + boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme(); + + if (type == DATE_SEPARATOR) { + if (UIHelper.today(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.today); + } else if (UIHelper.yesterday(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.yesterday); + } else { + viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); + } + viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); + return view; + } else if (type == RTP_SESSION) { + final boolean isDarkTheme = activity.isDarkTheme(); + final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; + final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final long duration = rtpSessionStatus.duration; + if (received) { + if (duration > 0) { + viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration, TimeframeUtils.resolve(activity,duration))); + } else { + viewHolder.status_message.setText(R.string.incoming_call); + } + } else { + if (duration > 0) { + viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration, TimeframeUtils.resolve(activity,duration))); + } else { + viewHolder.status_message.setText(R.string.outgoing_call); + } + } + viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received,rtpSessionStatus.successful,isDarkTheme)); + viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f); + viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); + return view; + } else if (type == STATUS) { + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.status_message.setVisibility(View.GONE); + viewHolder.contact_picture.setVisibility(View.GONE); + viewHolder.load_more_messages.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); + } else { + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setVisibility(View.GONE); + viewHolder.status_message.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + showAvatar = true; + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) { + showAvatar = true; + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + } else { + viewHolder.contact_picture.setVisibility(View.GONE); + } + } + return view; + } else { + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); + } + + resetClickListener(viewHolder.message_box, viewHolder.messageBody); + + viewHolder.contact_picture.setOnClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } + + }); + viewHolder.contact_picture.setOnLongClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(v, message); + return true; + } else { + return false; + } + }); + + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { + if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground); + } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); + } else { + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground); + } + } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { + displayMediaPreviewMessage(viewHolder, message, darkBackground); + } else if (message.getFileParams().runtime > 0) { + displayAudioMessage(viewHolder, message, darkBackground); + } else { + displayOpenableMessage(viewHolder, message, darkBackground); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (account.isPgpDecryptionServiceConnected()) { + if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { + displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); + } + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground); + viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { + displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground); + } else { + if (message.isGeoUri()) { + displayLocationMessage(viewHolder, message, darkBackground); + } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { + displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); + } else if (message.treatAsDownloadable()) { + try { + URL url = new URL(message.getBody()); + if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + darkBackground); + } else { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + url.getHost()), + darkBackground); + } + } catch (Exception e) { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + darkBackground); + } + } else { + displayTextMessage(viewHolder, message, darkBackground, type); + } + } + + if (type == RECEIVED) { + if (isInValidSession) { + int bubble; + if (!mUseGreenBackground) { + bubble = activity.getThemeResource(R.attr.message_bubble_received_monochrome, R.drawable.message_bubble_received_white); + } else { + bubble = activity.getThemeResource(R.attr.message_bubble_received_green, R.drawable.message_bubble_received); + } + viewHolder.message_box.setBackgroundResource(bubble); + viewHolder.encryption.setVisibility(View.GONE); + } else { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); + viewHolder.encryption.setVisibility(View.VISIBLE); + if (omemoEncryption && !message.isTrusted()) { + viewHolder.encryption.setText(R.string.not_trusted); + } else { + viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); + } + } + } + + displayStatus(viewHolder, message, type, darkBackground); + + return view; + } + + private void promptOpenKeychainInstall(View view) { + activity.showInstallPgpDialog(); + } + + @Override + public void notifyDataSetChanged() { + listSelectionManager.onBeforeNotifyDataSetChanged(); + super.notifyDataSetChanged(); + listSelectionManager.onAfterNotifyDataSetChanged(); + } + + private String transformText(CharSequence text, int start, int end, boolean forCopy) { + SpannableStringBuilder builder = new SpannableStringBuilder(text); + Object copySpan = new Object(); + builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); + for (DividerSpan dividerSpan : dividerSpans) { + builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), + dividerSpan.isLarge() ? "\n\n" : "\n"); + } + start = builder.getSpanStart(copySpan); + end = builder.getSpanEnd(copySpan); + if (start == -1 || end == -1) return ""; + builder = new SpannableStringBuilder(builder, start, end); + if (forCopy) { + QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); + for (QuoteSpan quoteSpan : quoteSpans) { + builder.insert(builder.getSpanStart(quoteSpan), "> "); + } + } + return builder.toString(); + } + + @Override + public String transformTextForCopy(CharSequence text, int start, int end) { + if (text instanceof Spanned) { + return transformText(text, start, end, true); + } else { + return text.toString().substring(start, end); + } + } + + public FileBackend getFileBackend() { + return activity.xmppConnectionService.getFileBackend(); + } + + public void stopAudioPlayer() { + audioPlayer.stop(); + } + + public void unregisterListenerInAudioPlayer() { + audioPlayer.unregisterListener(); + } + + public void startStopPending() { + audioPlayer.startStopPending(); + } + + public void openDownloadable(Message message) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ConversationFragment.registerPendingMessage(activity, message); + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE); + return; + } + final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + ViewUtil.view(activity, file); + } + + private void showLocation(Message message) { + for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show(); + } + + public void updatePreferences() { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); + this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background)); + } + + + public void setHighlightedTerm(List terms) { + this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); + } + + public interface OnQuoteListener { + void onQuote(String text); + } + + public interface OnContactPictureClicked { + void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + void onContactPictureLongClicked(View v, Message message); + } + + private static class ViewHolder { + + public Button load_more_messages; + public ImageView edit_indicator; + public RelativeLayout audioPlayer; + protected LinearLayout message_box; + protected Button download_button; + protected ImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected CopyTextView messageBody; + protected ImageView contact_picture; + protected TextView status_message; + protected TextView encryption; + } + + + private class MessageBodyActionModeCallback implements ActionMode.Callback { + + private final TextView textView; + + public MessageBodyActionModeCallback(TextView textView) { + this.textView = textView; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (onQuoteListener != null) { + int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); + // 3rd item is placed after "copy" item + menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == android.R.id.button1) { + int start = textView.getSelectionStart(); + int end = textView.getSelectionEnd(); + if (end > start) { + String text = transformText(textView.getText(), start, end, false); + if (onQuoteListener != null) { + onQuoteListener.onQuote(text); + } + mode.finish(); + } + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 47fec58c6e45ce81cced6424c36560a89b56879e..908e572cd62a6d935eb3a6ef6501d8186a2c1c40 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -299,6 +299,8 @@ public class UIHelper { return new Pair<>(context.getString(R.string.omemo_decryption_failed), true); } else if (message.isFileOrImage()) { return new Pair<>(getFileDescriptionString(context, message), true); + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return new Pair<>(context.getString(message.getStatus() == Message.STATUS_RECEIVED ? R.string.incoming_call : R.string.outgoing_call), true); } else { final String body = MessageUtils.filterLtrRtl(message.getBody()); if (body.startsWith(Message.ME_COMMAND)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e21e69a3d7b6a9801fb69360fdc5c517078245fb..a375ac657f0bf4beec9bf374ba1662d003e94fd2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.os.SystemClock; import android.util.Log; import com.google.common.base.Strings; @@ -18,6 +19,10 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; @@ -94,13 +99,27 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); + private final Message message; private State state = State.NULL; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; + private long rtpConnectionStarted = 0; //time of 'connected' JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); + final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( + id.account, + id.with.asBareJid(), + false, + false + ); + this.message = new Message( + conversation, + isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + id.sessionId + ); } private static State reasonToState(Reason reason) { @@ -153,7 +172,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } webRTCWrapper.close(); - transitionOrThrow(reasonToState(wrapper.reason)); + final State target = reasonToState(wrapper.reason); + transitionOrThrow(target); + writeLogMessage(target); if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } @@ -455,7 +476,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.RETRACTED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); - //TODO create missed call notification/message + writeLogMessageMissed(); jingleConnectionManager.finishConnection(this); } else { Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); @@ -509,6 +530,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionTerminate(final Reason reason, final String text) { final State target = reasonToState(reason); transitionOrThrow(target); + writeLogMessage(target); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); send(jinglePacket); @@ -773,9 +795,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); + if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { + this.rtpConnectionStarted = SystemClock.elapsedRealtime(); + } if (newState == PeerConnection.PeerConnectionState.FAILED) { if (TERMINATED.contains(this.state)) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": not sending session-terminate after connectivity error because session is already in state "+this.state); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); @@ -850,6 +875,37 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void writeLogMessage(final State state) { + final long started = this.rtpConnectionStarted; + long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; + if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { + writeLogMessageSuccess(duration); + } else { + writeLogMessageMissed(); + } + } + + private void writeLogMessageSuccess(final long duration) { + this.message.setBody(new RtpSessionStatus(true, duration).toString()); + this.writeMessage(); + } + + private void writeLogMessageMissed() { + this.message.setBody(new RtpSessionStatus(false,0).toString()); + this.writeMessage(); + } + + private void writeMessage() { + final Conversational conversational = message.getConversation(); + if (conversational instanceof Conversation) { + ((Conversation) conversational).add(this.message); + xmppConnectionService.databaseBackend.createMessage(message); + xmppConnectionService.updateConversationUi(); + } else { + throw new IllegalStateException("Somehow the conversation in a message was a stub"); + } + } + public State getState() { return this.state; } diff --git a/src/main/res/drawable-hdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1ef5b84e9112caae702fd957a79fba148ff2eea2 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_made_black_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1b126d2dc570a79242b6df3a89efd8640a1e5d5b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_made_white_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5447aedc84cd49661004adfa8437e8badbddbb43 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7cba4f0ac614a35761cd24e376fcbf3b218588aa Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..596012560cb23ea0c8d17446e7f47367f297d95a Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..81a7aa16c5bbc8fd032dbb5cf2f4bfbc2f391476 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..af45ceb84180e770986ff0437eeea12c257e59ed Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_received_black_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8080c7879fecde2727d0df1096f3930d3ad1b3b8 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_call_received_white_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e7293a0a908533354492db32075a495fce03c286 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_made_black_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6a23529c5c3178c8c8a59cde53fd8fbd911e43 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_made_white_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fb61875a19757ce05b597833a640e686e02bd1ab Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4ad9622324dde04abeff9ba9ebbdc0f5d525b1 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe42b28cb80dce13ff80b2712a2fa481c6ce8fb Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6d77962a322eb08c404b91ac1022bc2def1eff Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fe45b55175eb4c6ab1e2bab7687b0465a432c3de Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_received_black_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9accbbf36a0730c893b118333b8222d619873f3c Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_call_received_white_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..01cceead35b95f48595e406dde7072d435cfc84d Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ea6a8ab5f2382f8641bd014b7b0e12b0f4e936e8 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..930fa4373f3af1007419f0cc4cfc42fab24989e7 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e222c17fd06b254061b74414ba628afe871ef6d3 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fc9bc8b77a700ade84b8a974eaad93c42eb0f7 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f188eb9aa54f0460d80220a12bfd0f1b17f07db4 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4008ba956eb4e269434852097e63bc7713eb82c2 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2ae411a8f33dc64afd772e02183c2f911b2ee1 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2b55b174d34b2b10c4d426c3d6dccea5162ace Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..41a58b948ebca9850665dd0d733a9656010717b1 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2326b5fba588accecb7535c4f22caa4b1e2b4e4e Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f8e0746bb7db0f081d0fed08b4de54e45700d1 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a34511a03c4283ab0a8df85f70db9470a6177db5 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a615f59d562db6b916e5e724e6697d3f813eae5d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_missed_white_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..75b98ec1cb1816263ca18bcb15cfdbfa96f36b6e Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..13d588caf711c39f6dd8cce11b980c84fbbcc2b0 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d00c654c52ed0fd311bba2d2be07db95f096cf Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ae471c9fc5382d6ac8744e2b475b2867da7438ce Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_made_white_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b689b79cb2f1719e46ba6cd8fe7d4eee0f34eee9 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_missed_black_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5117934594e1692df7bdb1ae20a252c9b3fe861e Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..232460ca1b1667bacff047ce45789edc5804d6bf Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_white_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2374dc5a1158455c8a7ccfc02d821e2144e9952b Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..81dc0c36797531a9ed390414b571d9c24223da61 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..58421114fd7acefd4713ed01e079a58747d96a7e Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png differ diff --git a/src/main/res/layout/message_date_bubble.xml b/src/main/res/layout/message_date_bubble.xml index 37d43bd3b78077572de3253cd3a22a0165112e5f..5e5cd0c4d9260034cc638b5cac9f475499c81c08 100644 --- a/src/main/res/layout/message_date_bubble.xml +++ b/src/main/res/layout/message_date_bubble.xml @@ -1,24 +1,27 @@ + android:paddingBottom="5dp"> + android:layout_centerHorizontal="true" + android:background="@drawable/date_bubble_white"> + + tools:text="Yesterday" /> \ No newline at end of file diff --git a/src/main/res/layout/message_rtp_session.xml b/src/main/res/layout/message_rtp_session.xml new file mode 100644 index 0000000000000000000000000000000000000000..ad7f06d6a74f9d734bf70cacc91d73e92738f946 --- /dev/null +++ b/src/main/res/layout/message_rtp_session.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ef2267a4a11ac56b953bbb7ce10f2afcfaf2266e..f353145b0400bb1dd4c4b0a0a65c629a54c37adc 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -903,6 +903,10 @@ Hang up Ongoing call Disable Tor to make calls + Incoming call + Incoming call · %s + Outgoing call + Outgoing call · %s View %1$d Participant View %1$d Participants