conversations-classic/src/main/java/eu/siacs/conversations/entities/Message.java

1005 lines
29 KiB
Java
Raw Normal View History

2014-02-28 17:46:01 +00:00
package eu.siacs.conversations.entities;
2014-01-24 01:04:05 +00:00
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Color;
2016-10-19 21:53:13 +00:00
import android.text.SpannableStringBuilder;
2018-12-01 14:52:44 +00:00
import android.util.Log;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
2018-12-01 14:52:44 +00:00
import org.json.JSONException;
2018-04-12 07:50:33 +00:00
import java.lang.ref.WeakReference;
2014-10-13 23:06:45 +00:00
import java.net.MalformedURLException;
import java.net.URL;
2018-12-01 14:52:44 +00:00
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
2014-10-13 23:06:45 +00:00
2014-08-31 16:21:46 +00:00
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils;
2015-07-01 14:01:18 +00:00
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
2020-05-15 15:06:16 +00:00
import eu.siacs.conversations.xmpp.Jid;
public class Message extends AbstractEntity implements AvatarService.Avatarable {
2014-08-31 12:29:12 +00:00
2014-01-26 02:27:55 +00:00
public static final String TABLENAME = "messages";
2014-08-28 09:01:24 +00:00
public static final int STATUS_RECEIVED = 0;
public static final int STATUS_UNSEND = 1;
public static final int STATUS_SEND = 2;
2014-04-11 07:13:56 +00:00
public static final int STATUS_SEND_FAILED = 3;
2014-06-12 21:04:28 +00:00
public static final int STATUS_WAITING = 5;
public static final int STATUS_OFFERED = 6;
public static final int STATUS_SEND_RECEIVED = 7;
public static final int STATUS_SEND_DISPLAYED = 8;
public static final int ENCRYPTION_NONE = 0;
public static final int ENCRYPTION_PGP = 1;
public static final int ENCRYPTION_OTR = 2;
2014-02-27 23:22:56 +00:00
public static final int ENCRYPTION_DECRYPTED = 3;
2014-05-01 20:33:49 +00:00
public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
public static final int ENCRYPTION_AXOLOTL = 5;
public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
2014-08-31 12:29:12 +00:00
2014-04-06 13:34:08 +00:00
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
2014-11-13 20:04:05 +00:00
public static final int TYPE_FILE = 2;
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;
2015-01-11 15:22:29 +00:00
public static final String CONVERSATION = "conversationUuid";
public static final String COUNTERPART = "counterpart";
public static final String TRUE_COUNTERPART = "trueCounterpart";
public static final String BODY = "body";
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
public static final String BODY_LANGUAGE = "bodyLanguage";
2015-01-11 15:22:29 +00:00
public static final String TIME_SENT = "timeSent";
public static final String ENCRYPTION = "encryption";
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String CARBON = "carbon";
public static final String OOB = "oob";
public static final String EDITED = "edited";
2015-01-11 15:22:29 +00:00
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String READ = "read";
public static final String ERROR_MESSAGE = "errorMsg";
public static final String READ_BY_MARKERS = "readByMarkers";
2017-11-25 19:55:43 +00:00
public static final String MARKABLE = "markable";
public static final String DELETED = "deleted";
2015-01-25 15:29:26 +00:00
public static final String ME_COMMAND = "/me ";
2015-01-11 15:22:29 +00:00
public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
public boolean markable = false;
protected String conversationUuid;
protected Jid counterpart;
protected Jid trueCounterpart;
protected String body;
2014-04-03 15:39:57 +00:00
protected String encryptedBody;
protected long timeSent;
protected int encryption;
protected int status;
2014-04-06 13:34:08 +00:00
protected int type;
protected boolean deleted = false;
protected boolean carbon = false;
protected boolean oob = false;
protected List<Edit> edits = new ArrayList<>();
2014-11-13 20:04:05 +00:00
protected String relativeFilePath;
2014-02-10 21:45:59 +00:00
protected boolean read = true;
protected String remoteMsgId = null;
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
private String bodyLanguage = null;
protected String serverMsgId = null;
private final Conversational conversation;
2015-07-10 13:11:03 +00:00
protected Transferable transferable = null;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private String errorMessage = null;
private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
private Boolean isGeoUri = null;
private Boolean isEmojisOnly = null;
private Boolean treatAsDownloadable = null;
private FileParams fileParams = null;
private List<MucOptions.User> counterparts;
2018-04-12 07:50:33 +00:00
private WeakReference<MucOptions.User> user;
protected Message(Conversational conversation) {
this.conversation = conversation;
}
public Message(Conversational conversation, String body, int encryption) {
2014-11-18 13:49:49 +00:00
this(conversation, body, encryption, STATUS_UNSEND);
}
public Message(Conversational conversation, String body, int encryption, int status) {
this(conversation, java.util.UUID.randomUUID().toString(),
2014-12-01 09:58:06 +00:00
conversation.getUuid(),
2018-03-05 17:30:40 +00:00
conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
2014-12-01 09:58:06 +00:00
null,
body,
System.currentTimeMillis(),
encryption,
status,
TYPE_TEXT,
false,
2014-12-01 09:58:06 +00:00
null,
null,
null,
null,
true,
null,
false,
null,
2017-11-25 19:55:43 +00:00
null,
false,
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
false,
null);
2014-02-01 14:07:20 +00:00
}
2014-08-31 12:29:12 +00:00
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,
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read,
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
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
final boolean markable, final boolean deleted, final String bodyLanguage) {
this.conversation = conversation;
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
this.trueCounterpart = trueCounterpart;
this.body = body == null ? "" : body;
this.timeSent = timeSent;
this.encryption = encryption;
this.status = status;
2014-04-06 13:34:08 +00:00
this.type = type;
this.carbon = carbon;
this.remoteMsgId = remoteMsgId;
2014-11-13 20:04:05 +00:00
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
this.read = read;
this.edits = Edit.fromJson(edited);
this.oob = oob;
this.errorMessage = errorMessage;
this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
2017-11-25 19:55:43 +00:00
this.markable = markable;
this.deleted = deleted;
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
this.bodyLanguage = bodyLanguage;
}
public static Message fromCursor(Cursor cursor, Conversation conversation) {
return new Message(conversation,
cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
cursor.getString(cursor.getColumnIndex(BODY)),
cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
2014-11-13 20:04:05 +00:00
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)),
cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
2017-11-25 19:55:43 +00:00
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
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
cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
);
}
private static Jid fromString(String value) {
try {
if (value != null) {
return Jid.of(value);
}
} catch (IllegalArgumentException e) {
return null;
}
return null;
}
public static Message createStatusMessage(Conversation conversation, String body) {
final Message message = new Message(conversation);
message.setType(Message.TYPE_STATUS);
2017-08-21 14:19:35 +00:00
message.setStatus(Message.STATUS_RECEIVED);
message.body = body;
return message;
}
public static Message createLoadMoreMessage(Conversation conversation) {
final Message message = new Message(conversation);
message.setType(Message.TYPE_STATUS);
message.body = "LOAD_MORE";
return message;
}
@Override
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
2014-01-28 18:21:54 +00:00
values.put(UUID, uuid);
values.put(CONVERSATION, conversationUuid);
if (counterpart == null) {
values.putNull(COUNTERPART);
} else {
2018-03-05 17:30:40 +00:00
values.put(COUNTERPART, counterpart.toString());
}
2014-11-18 13:49:49 +00:00
if (trueCounterpart == null) {
values.putNull(TRUE_COUNTERPART);
} else {
2018-03-05 17:30:40 +00:00
values.put(TRUE_COUNTERPART, trueCounterpart.toString());
}
values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
values.put(TIME_SENT, timeSent);
values.put(ENCRYPTION, encryption);
values.put(STATUS, status);
2014-04-06 13:34:08 +00:00
values.put(TYPE, type);
values.put(CARBON, carbon ? 1 : 0);
2014-08-31 12:29:12 +00:00
values.put(REMOTE_MSG_ID, remoteMsgId);
2014-11-13 20:04:05 +00:00
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
values.put(READ, read ? 1 : 0);
2018-12-01 14:52:44 +00:00
try {
values.put(EDITED, Edit.toJson(edits));
2018-12-01 14:52:44 +00:00
} catch (JSONException e) {
Log.e(Config.LOGTAG,"error persisting json for edits",e);
}
values.put(OOB, oob ? 1 : 0);
values.put(ERROR_MESSAGE, errorMessage);
values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
2017-11-25 19:55:43 +00:00
values.put(MARKABLE, markable ? 1 : 0);
values.put(DELETED, deleted ? 1 : 0);
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
values.put(BODY_LANGUAGE, bodyLanguage);
return values;
2014-01-24 01:04:05 +00:00
}
public String getConversationUuid() {
return conversationUuid;
}
2014-08-31 12:29:12 +00:00
public Conversational getConversation() {
return this.conversation;
}
public Jid getCounterpart() {
return counterpart;
}
2014-08-31 12:29:12 +00:00
public void setCounterpart(final Jid counterpart) {
this.counterpart = counterpart;
}
public Contact getContact() {
if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
return this.conversation.getContact();
} else {
if (this.trueCounterpart == null) {
return null;
} else {
return this.conversation.getAccount().getRoster()
.getContactFromContactList(this.trueCounterpart);
}
}
}
2014-08-31 14:28:21 +00:00
public String getBody() {
return body;
}
2014-08-31 12:29:12 +00:00
public synchronized void setBody(String body) {
if (body == null) {
throw new Error("You should not set the message body to null");
}
this.body = body;
this.isGeoUri = null;
this.isEmojisOnly = null;
this.treatAsDownloadable = null;
this.fileParams = null;
}
2018-04-12 07:50:33 +00:00
public void setMucUser(MucOptions.User user) {
this.user = new WeakReference<>(user);
}
public boolean sameMucUser(Message otherMessage) {
final MucOptions.User thisUser = this.user == null ? null : this.user.get();
final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
return thisUser != null && thisUser == otherUser;
}
public String getErrorMessage() {
return errorMessage;
}
public boolean setErrorMessage(String message) {
boolean changed = (message != null && !message.equals(errorMessage))
|| (message == null && errorMessage != null);
this.errorMessage = message;
return changed;
}
public long getTimeSent() {
return timeSent;
}
public int getEncryption() {
return encryption;
}
public void setEncryption(int encryption) {
this.encryption = encryption;
}
public int getStatus() {
return status;
2014-01-24 01:04:05 +00:00
}
2014-08-31 12:29:12 +00:00
public void setStatus(int status) {
this.status = status;
}
2014-11-18 13:49:49 +00:00
public String getRelativeFilePath() {
return this.relativeFilePath;
2014-11-13 20:04:05 +00:00
}
2014-11-18 13:49:49 +00:00
public void setRelativeFilePath(String path) {
this.relativeFilePath = path;
2014-11-13 20:04:05 +00:00
}
public String getRemoteMsgId() {
return this.remoteMsgId;
}
2014-08-31 12:29:12 +00:00
public void setRemoteMsgId(String id) {
this.remoteMsgId = id;
}
2014-01-24 01:04:05 +00:00
public String getServerMsgId() {
return this.serverMsgId;
}
public void setServerMsgId(String id) {
this.serverMsgId = id;
}
2014-02-10 21:45:59 +00:00
public boolean isRead() {
return this.read;
}
2014-08-31 12:29:12 +00:00
public boolean isDeleted() {
return this.deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
2014-02-10 21:45:59 +00:00
public void markRead() {
this.read = true;
}
2014-08-31 12:29:12 +00:00
2014-02-10 21:45:59 +00:00
public void markUnread() {
this.read = false;
}
public void setTime(long time) {
this.timeSent = time;
}
2014-02-16 15:32:15 +00:00
2014-04-03 15:39:57 +00:00
public String getEncryptedBody() {
return this.encryptedBody;
}
2014-08-31 12:29:12 +00:00
2014-04-03 15:39:57 +00:00
public void setEncryptedBody(String body) {
this.encryptedBody = body;
}
2014-04-06 13:34:08 +00:00
public int getType() {
return this.type;
}
public void setType(int type) {
this.type = type;
}
2014-08-31 12:29:12 +00:00
public boolean isCarbon() {
return carbon;
}
public void setCarbon(boolean carbon) {
this.carbon = carbon;
}
2018-12-01 14:52:44 +00:00
public void putEdited(String edited, String serverMsgId) {
final Edit edit = new Edit(edited, serverMsgId);
if (this.edits.size() < 128 && !this.edits.contains(edit)) {
this.edits.add(edit);
}
}
boolean remoteMsgIdMatchInEdit(String id) {
for(Edit edit : this.edits) {
2019-09-13 14:38:15 +00:00
if (id.equals(edit.getEditedId())) {
return true;
}
}
return false;
}
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
public String getBodyLanguage() {
return this.bodyLanguage;
}
public void setBodyLanguage(String language) {
this.bodyLanguage = language;
}
public boolean edited() {
2018-12-01 14:52:44 +00:00
return this.edits.size() > 0;
}
public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart;
}
2014-08-31 12:29:12 +00:00
public Jid getTrueCounterpart() {
return this.trueCounterpart;
}
2015-07-10 13:11:03 +00:00
public Transferable getTransferable() {
return this.transferable;
}
2014-08-31 12:29:12 +00:00
public synchronized void setTransferable(Transferable transferable) {
this.fileParams = null;
2015-07-10 13:11:03 +00:00
this.transferable = transferable;
}
2014-08-31 12:29:12 +00:00
public boolean addReadByMarker(ReadByMarker readByMarker) {
if (readByMarker.getRealJid() != null) {
2018-03-05 17:30:40 +00:00
if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
return false;
}
} else if (readByMarker.getFullJid() != null) {
if (readByMarker.getFullJid().equals(counterpart)) {
return false;
}
}
if (this.readByMarkers.add(readByMarker)) {
if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
while (iterator.hasNext()) {
ReadByMarker marker = iterator.next();
if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
iterator.remove();
}
}
}
return true;
} else {
return false;
}
}
public Set<ReadByMarker> getReadByMarkers() {
return ImmutableSet.copyOf(this.readByMarkers);
}
2019-01-21 10:55:52 +00:00
boolean similar(Message message) {
if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
2018-12-01 14:52:44 +00:00
return true;
2015-01-20 21:54:58 +00:00
} else if (this.body == null || this.counterpart == null) {
return false;
} else {
String body, otherBody;
if (this.hasFileOnRemoteHost()) {
body = getFileParams().url.toString();
otherBody = message.body == null ? null : message.body.trim();
} else {
body = this.body;
otherBody = message.body;
}
2016-12-26 14:13:38 +00:00
final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
if (message.getRemoteMsgId() != null) {
2016-12-26 14:13:38 +00:00
final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
2016-12-26 14:13:38 +00:00
return true;
}
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
2016-12-26 14:13:38 +00:00
&& matchingCounterpart
&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
} else {
return this.remoteMsgId == null
2016-12-26 14:13:38 +00:00
&& matchingCounterpart
&& body.equals(otherBody)
&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
}
}
2014-08-31 12:29:12 +00:00
}
public Message next() {
if (this.conversation instanceof Conversation) {
final Conversation conversation = (Conversation) this.conversation;
synchronized (conversation.messages) {
if (this.mNextMessage == null) {
int index = conversation.messages.indexOf(this);
if (index < 0 || index >= conversation.messages.size() - 1) {
this.mNextMessage = null;
} else {
this.mNextMessage = conversation.messages.get(index + 1);
}
}
return this.mNextMessage;
}
} else {
throw new AssertionError("Calling next should be disabled for stubs");
2014-08-31 12:29:12 +00:00
}
}
public Message prev() {
if (this.conversation instanceof Conversation) {
final Conversation conversation = (Conversation) this.conversation;
synchronized (conversation.messages) {
if (this.mPreviousMessage == null) {
int index = conversation.messages.indexOf(this);
if (index <= 0 || index > conversation.messages.size()) {
this.mPreviousMessage = null;
} else {
this.mPreviousMessage = conversation.messages.get(index - 1);
}
}
}
return this.mPreviousMessage;
} else {
throw new AssertionError("Calling prev should be disabled for stubs");
2014-08-31 12:29:12 +00:00
}
}
public boolean isLastCorrectableMessage() {
Message next = next();
while (next != null) {
if (next.isEditable()) {
return false;
}
next = next.next();
}
return isEditable();
}
public boolean isEditable() {
return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
}
2014-11-18 13:49:49 +00:00
public boolean mergeable(final Message message) {
2015-01-11 15:22:29 +00:00
return message != null &&
(message.getType() == Message.TYPE_TEXT &&
2015-07-10 13:11:03 +00:00
this.getTransferable() == null &&
message.getTransferable() == null &&
message.getEncryption() != Message.ENCRYPTION_PGP &&
2016-02-23 14:30:41 +00:00
message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
this.getType() == message.getType() &&
//this.getStatus() == message.getStatus() &&
isStatusMergeable(this.getStatus(), message.getStatus()) &&
this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
!message.isGeoUri() &&
!this.isGeoUri() &&
2020-03-02 10:10:38 +00:00
!message.isOOb() &&
!this.isOOb() &&
!message.treatAsDownloadable() &&
!this.treatAsDownloadable() &&
!message.hasMeCommand() &&
!this.hasMeCommand() &&
!this.bodyIsOnlyEmojis() &&
!message.bodyIsOnlyEmojis() &&
2017-07-04 09:01:20 +00:00
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
2018-02-25 13:28:14 +00:00
this.getReadByMarkers().equals(message.getReadByMarkers()) &&
!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
);
2014-08-31 12:29:12 +00:00
}
private static boolean isStatusMergeable(int a, int b) {
return a == b || (
(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
);
}
public void setCounterparts(List<MucOptions.User> counterparts) {
this.counterparts = counterparts;
}
public List<MucOptions.User> getCounterparts() {
return this.counterparts;
}
@Override
public int getAvatarBackgroundColor() {
if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
return Color.TRANSPARENT;
} else {
return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
}
}
@Override
public String getAvatarName() {
return UIHelper.getMessageDisplayName(this);
}
public boolean isOOb() {
return oob;
}
public static class MergeSeparator {
}
2016-10-19 21:53:13 +00:00
public SpannableStringBuilder getMergedBody() {
SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
Message current = this;
2016-10-19 21:53:13 +00:00
while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
2016-10-19 21:53:13 +00:00
body.append("\n\n");
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
2014-08-31 12:29:12 +00:00
}
2016-10-19 21:53:13 +00:00
return body;
2014-08-31 12:29:12 +00:00
}
2015-01-11 15:22:29 +00:00
public boolean hasMeCommand() {
2016-10-19 21:53:13 +00:00
return this.body.trim().startsWith(ME_COMMAND);
2015-01-11 15:22:29 +00:00
}
public int getMergedStatus() {
int status = this.status;
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
status = current.status;
}
return status;
}
public long getMergedTimeSent() {
long time = this.timeSent;
Message current = this;
while (current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
time = current.timeSent;
}
return time;
}
2014-08-31 12:29:12 +00:00
public boolean wasMergedIntoPrevious() {
Message prev = this.prev();
return prev != null && prev.mergeable(this);
2014-08-23 13:57:39 +00:00
}
public boolean trusted() {
Contact contact = this.getContact();
return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
}
public boolean fixCounterpart() {
final Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
return true;
} else if (presences.size() >= 1) {
counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
return true;
} else {
counterpart = null;
return false;
}
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getEditedId() {
2018-12-01 14:52:44 +00:00
if (edits.size() > 0) {
return edits.get(edits.size() - 1).getEditedId();
2019-09-13 14:38:15 +00:00
} else {
throw new IllegalStateException("Attempting to store unedited message");
}
}
public String getEditedIdWireFormat() {
if (edits.size() > 0) {
return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
2018-12-01 14:52:44 +00:00
} else {
throw new IllegalStateException("Attempting to store unedited message");
}
}
public void setOob(boolean isOob) {
this.oob = isOob;
}
2015-07-01 14:01:18 +00:00
public String getMimeType() {
String extension;
2015-07-01 14:01:18 +00:00
if (relativeFilePath != null) {
extension = MimeUtils.extractRelevantExtension(relativeFilePath);
2015-07-01 14:01:18 +00:00
} else {
try {
final URL url = new URL(body.split("\n")[0]);
extension = MimeUtils.extractRelevantExtension(url);
2015-07-01 14:01:18 +00:00
} catch (MalformedURLException e) {
return null;
}
}
return MimeUtils.guessMimeTypeFromExtension(extension);
}
public synchronized boolean treatAsDownloadable() {
if (treatAsDownloadable == null) {
treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
2014-10-13 23:06:45 +00:00
}
return treatAsDownloadable;
2014-10-13 23:06:45 +00:00
}
public synchronized boolean bodyIsOnlyEmojis() {
if (isEmojisOnly == null) {
isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
}
return isEmojisOnly;
}
public synchronized boolean isGeoUri() {
if (isGeoUri == null) {
isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
}
return isGeoUri;
}
public synchronized void resetFileParams() {
this.fileParams = null;
}
public synchronized FileParams getFileParams() {
if (fileParams == null) {
fileParams = new FileParams();
if (this.transferable != null) {
fileParams.size = this.transferable.getFileSize();
}
2019-12-26 16:36:16 +00:00
final String[] parts = body == null ? new String[0] : body.split("\\|");
switch (parts.length) {
case 1:
try {
fileParams.size = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
fileParams.url = parseUrl(parts[0]);
}
break;
case 5:
fileParams.runtime = parseInt(parts[4]);
case 4:
fileParams.width = parseInt(parts[2]);
fileParams.height = parseInt(parts[3]);
case 2:
fileParams.url = parseUrl(parts[0]);
fileParams.size = parseLong(parts[1]);
break;
case 3:
fileParams.size = parseLong(parts[0]);
fileParams.width = parseInt(parts[1]);
fileParams.height = parseInt(parts[2]);
break;
}
2014-10-14 16:16:03 +00:00
}
return fileParams;
2014-10-14 16:16:03 +00:00
}
private static long parseLong(String value) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return 0;
}
}
private static int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return 0;
}
}
private static URL parseUrl(String value) {
try {
return new URL(value);
} catch (MalformedURLException e) {
return null;
}
}
public void untie() {
this.mNextMessage = null;
this.mPreviousMessage = null;
}
public boolean isPrivateMessage() {
return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
}
2015-01-02 13:27:49 +00:00
public boolean isFileOrImage() {
return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
2015-01-02 13:27:49 +00:00
}
public boolean hasFileOnRemoteHost() {
return isFileOrImage() && getFileParams().url != null;
}
public boolean needsUploading() {
return isFileOrImage() && getFileParams().url == null;
}
2021-03-16 07:12:30 +00:00
public static class FileParams {
public URL url;
2014-10-14 16:16:03 +00:00
public long size = 0;
public int width = 0;
public int height = 0;
public int runtime = 0;
2014-10-14 16:16:03 +00:00
}
2016-03-31 19:15:49 +00:00
public void setFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
2016-03-31 19:15:49 +00:00
public String getFingerprint() {
return axolotlFingerprint;
}
public boolean isTrusted() {
FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
return s != null && s.isTrusted();
}
private int getPreviousEncryption() {
for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
continue;
}
return iterator.getEncryption();
}
return ENCRYPTION_NONE;
}
private int getNextEncryption() {
if (this.conversation instanceof Conversation) {
Conversation conversation = (Conversation) this.conversation;
for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
continue;
}
return iterator.getEncryption();
}
return conversation.getNextEncryption();
} else {
throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
}
}
public boolean isValidInSession() {
int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
int futureEncryption = getCleanedEncryption(this.getNextEncryption());
boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
|| futureEncryption == ENCRYPTION_NONE
|| pastEncryption != futureEncryption;
return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
}
private static int getCleanedEncryption(int encryption) {
if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
return ENCRYPTION_PGP;
}
if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
return ENCRYPTION_AXOLOTL;
}
return encryption;
}
public static boolean configurePrivateMessage(final Message message) {
return configurePrivateMessage(message, false);
}
public static boolean configurePrivateFileMessage(final Message message) {
return configurePrivateMessage(message, true);
}
private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
final Conversation conversation;
if (message.conversation instanceof Conversation) {
conversation = (Conversation) message.conversation;
} else {
return false;
}
if (conversation.getMode() == Conversation.MODE_MULTI) {
final Jid nextCounterpart = conversation.getNextCounterpart();
if (nextCounterpart != null) {
message.setCounterpart(nextCounterpart);
message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
return true;
}
}
return false;
}
2014-01-24 01:04:05 +00:00
}