conversations-classic/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java

1032 lines
51 KiB
Java
Raw Normal View History

package eu.siacs.conversations.ui.adapter;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
2018-02-21 14:46:56 +00:00
import android.content.SharedPreferences;
2014-11-15 14:16:40 +00:00
import android.content.pm.PackageManager;
import android.graphics.Typeface;
2018-02-21 14:46:56 +00:00
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
2014-08-22 11:22:07 +00:00
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
2017-07-04 09:01:20 +00:00
import android.text.format.DateUtils;
2014-08-22 11:22:07 +00:00
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
2014-08-22 11:22:07 +00:00
import android.text.style.StyleSpan;
import android.util.DisplayMetrics;
2016-11-25 14:06:43 +00:00
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
2014-11-07 14:38:20 +00:00
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
2017-09-20 13:35:35 +00:00
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.base.Strings;
2016-03-04 20:30:34 +00:00
import java.net.URL;
2014-11-07 14:38:20 +00:00
import java.util.List;
show language in message bubble if multiple language variants were received XML and by inheritence XMPP has the feature of transmitting multiple language variants for the same content. This can be really useful if, for example, you are talking to an automated system. A chat bot could greet you in your own language. On the wire this will usually look like this: ```xml <message to="you"> <body>Good morning</body> <body xml:lang="de">Guten Morgen</body> </message> ``` However receiving such a message in a group chat can be very confusing and potentially dangerous if the sender puts conflicting information in there and different people get shown different strings. Disabeling support for localization entirely isn’t an ideal solution as on principle it is still a good feature; and other clients might still show a localization even if Conversations would always show the default language. So instead Conversations now shows the displayed language in a corner of the message bubble if more than one translation has been received. If multiple languages are received Conversations will attempt to find one in the language the operating system is set to. If no such translation can be found it will attempt to display the English string. If English can not be found either (for example a message that only has ru and fr on a phone that is set to de) it will display what ever language came first. Furthermore Conversations will discard (not show at all) messages with with multiple bodies of the same language. (This is considered an invalid message) The lanuage tag will not be shown if Conversations received a single body in a language not understood by the user. (For example operating system set to 'de' and message received with one body in 'ru' will just display that body as usual.) As a guide line to the user: If you are reading a message where it is important that this message is not interpreted differently by different people (like a vote (+1 / -1) in a chat room) make sure it has *no* language tag.
2019-09-12 08:12:47 +00:00
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
2014-11-07 14:38:20 +00:00
import eu.siacs.conversations.Config;
2014-11-07 14:38:20 +00:00
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
2014-12-03 09:35:30 +00:00
import eu.siacs.conversations.entities.Account;
2014-11-07 14:38:20 +00:00
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
2014-11-13 20:04:05 +00:00
import eu.siacs.conversations.entities.DownloadableFile;
2014-11-07 14:38:20 +00:00
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.RtpSessionStatus;
2015-07-20 12:26:29 +00:00
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.NotificationService;
import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.ui.ConversationsActivity;
2018-02-21 14:46:56 +00:00
import eu.siacs.conversations.ui.XmppActivity;
2017-09-20 13:35:35 +00:00
import eu.siacs.conversations.ui.service.AudioPlayer;
2016-11-25 14:06:43 +00:00
import eu.siacs.conversations.ui.text.DividerSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MyLinkify;
import eu.siacs.conversations.ui.util.ViewUtil;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
import eu.siacs.conversations.ui.widget.CopyTextView;
import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.EmojiWrapper;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils;
2017-11-05 20:34:40 +00:00
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.TimeframeUtils;
2014-11-07 14:38:20 +00:00
import eu.siacs.conversations.utils.UIHelper;
2017-05-07 19:05:35 +00:00
import eu.siacs.conversations.xmpp.mam.MamReference;
import rocks.xmpp.addr.Jid;
2014-11-07 14:38:20 +00:00
public class MessageAdapter extends ArrayAdapter<Message> 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 static final int RTP_SESSION = 4;
private final XmppActivity activity;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
private final AudioPlayer audioPlayer;
private List<String> highlightedTerm = null;
private DisplayMetrics metrics;
private OnContactPictureClicked mOnContactPictureClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mUseGreenBackground = false;
private OnQuoteListener onQuoteListener;
public MessageAdapter(XmppActivity activity, List<Message> 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);
2017-11-09 19:23:31 +00:00
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);
2015-01-11 15:22:29 +00:00
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 == 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<String> 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) {
}
}
}