Message.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.graphics.Color;
  6import android.text.SpannableStringBuilder;
  7import android.util.Log;
  8
  9import com.google.common.base.Strings;
 10
 11import org.json.JSONException;
 12
 13import java.lang.ref.WeakReference;
 14import java.net.MalformedURLException;
 15import java.net.URL;
 16import java.util.ArrayList;
 17import java.util.Collections;
 18import java.util.HashSet;
 19import java.util.Iterator;
 20import java.util.List;
 21import java.util.Set;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 25import eu.siacs.conversations.services.AvatarService;
 26import eu.siacs.conversations.ui.util.PresenceSelector;
 27import eu.siacs.conversations.utils.CryptoHelper;
 28import eu.siacs.conversations.utils.Emoticons;
 29import eu.siacs.conversations.utils.GeoHelper;
 30import eu.siacs.conversations.utils.MessageUtils;
 31import eu.siacs.conversations.utils.MimeUtils;
 32import eu.siacs.conversations.utils.UIHelper;
 33import eu.siacs.conversations.xmpp.Jid;
 34
 35public class Message extends AbstractEntity implements AvatarService.Avatarable  {
 36
 37	public static final String TABLENAME = "messages";
 38
 39	public static final int STATUS_RECEIVED = 0;
 40	public static final int STATUS_UNSEND = 1;
 41	public static final int STATUS_SEND = 2;
 42	public static final int STATUS_SEND_FAILED = 3;
 43	public static final int STATUS_WAITING = 5;
 44	public static final int STATUS_OFFERED = 6;
 45	public static final int STATUS_SEND_RECEIVED = 7;
 46	public static final int STATUS_SEND_DISPLAYED = 8;
 47
 48	public static final int ENCRYPTION_NONE = 0;
 49	public static final int ENCRYPTION_PGP = 1;
 50	public static final int ENCRYPTION_OTR = 2;
 51	public static final int ENCRYPTION_DECRYPTED = 3;
 52	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
 53	public static final int ENCRYPTION_AXOLOTL = 5;
 54	public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
 55	public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
 56
 57	public static final int TYPE_TEXT = 0;
 58	public static final int TYPE_IMAGE = 1;
 59	public static final int TYPE_FILE = 2;
 60	public static final int TYPE_STATUS = 3;
 61	public static final int TYPE_PRIVATE = 4;
 62	public static final int TYPE_PRIVATE_FILE = 5;
 63	public static final int TYPE_RTP_SESSION = 6;
 64
 65	public static final String CONVERSATION = "conversationUuid";
 66	public static final String COUNTERPART = "counterpart";
 67	public static final String TRUE_COUNTERPART = "trueCounterpart";
 68	public static final String BODY = "body";
 69	public static final String BODY_LANGUAGE = "bodyLanguage";
 70	public static final String TIME_SENT = "timeSent";
 71	public static final String ENCRYPTION = "encryption";
 72	public static final String STATUS = "status";
 73	public static final String TYPE = "type";
 74	public static final String CARBON = "carbon";
 75	public static final String OOB = "oob";
 76	public static final String EDITED = "edited";
 77	public static final String REMOTE_MSG_ID = "remoteMsgId";
 78	public static final String SERVER_MSG_ID = "serverMsgId";
 79	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 80	public static final String FINGERPRINT = "axolotl_fingerprint";
 81	public static final String READ = "read";
 82	public static final String ERROR_MESSAGE = "errorMsg";
 83	public static final String READ_BY_MARKERS = "readByMarkers";
 84	public static final String MARKABLE = "markable";
 85	public static final String DELETED = "deleted";
 86	public static final String ME_COMMAND = "/me ";
 87
 88	public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
 89
 90
 91	public boolean markable = false;
 92	protected String conversationUuid;
 93	protected Jid counterpart;
 94	protected Jid trueCounterpart;
 95	protected String body;
 96	protected String encryptedBody;
 97	protected long timeSent;
 98	protected int encryption;
 99	protected int status;
100	protected int type;
101	protected boolean deleted = false;
102	protected boolean carbon = false;
103	protected boolean oob = false;
104	protected List<Edit> edits = new ArrayList<>();
105	protected String relativeFilePath;
106	protected boolean read = true;
107	protected String remoteMsgId = null;
108	private String bodyLanguage = null;
109	protected String serverMsgId = null;
110	private final Conversational conversation;
111	protected Transferable transferable = null;
112	private Message mNextMessage = null;
113	private Message mPreviousMessage = null;
114	private String axolotlFingerprint = null;
115	private String errorMessage = null;
116	private Set<ReadByMarker> readByMarkers = new HashSet<>();
117
118	private Boolean isGeoUri = null;
119	private Boolean isEmojisOnly = null;
120	private Boolean treatAsDownloadable = null;
121	private FileParams fileParams = null;
122	private List<MucOptions.User> counterparts;
123	private WeakReference<MucOptions.User> user;
124
125	protected Message(Conversational conversation) {
126		this.conversation = conversation;
127	}
128
129	public Message(Conversational conversation, String body, int encryption) {
130		this(conversation, body, encryption, STATUS_UNSEND);
131	}
132
133	public Message(Conversational conversation, String body, int encryption, int status) {
134		this(conversation, java.util.UUID.randomUUID().toString(),
135				conversation.getUuid(),
136				conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
137				null,
138				body,
139				System.currentTimeMillis(),
140				encryption,
141				status,
142				TYPE_TEXT,
143				false,
144				null,
145				null,
146				null,
147				null,
148				true,
149				null,
150				false,
151				null,
152				null,
153				false,
154				false,
155				null);
156	}
157
158	public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
159		this(conversation, java.util.UUID.randomUUID().toString(),
160				conversation.getUuid(),
161				conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
162				null,
163				null,
164				System.currentTimeMillis(),
165				Message.ENCRYPTION_NONE,
166				status,
167				type,
168				false,
169				remoteMsgId,
170				null,
171				null,
172				null,
173				true,
174				null,
175				false,
176				null,
177				null,
178				false,
179				false,
180				null);
181	}
182
183	protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
184	                final Jid trueCounterpart, final String body, final long timeSent,
185	                final int encryption, final int status, final int type, final boolean carbon,
186	                final String remoteMsgId, final String relativeFilePath,
187	                final String serverMsgId, final String fingerprint, final boolean read,
188	                final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
189	                final boolean markable, final boolean deleted, final String bodyLanguage) {
190		this.conversation = conversation;
191		this.uuid = uuid;
192		this.conversationUuid = conversationUUid;
193		this.counterpart = counterpart;
194		this.trueCounterpart = trueCounterpart;
195		this.body = body == null ? "" : body;
196		this.timeSent = timeSent;
197		this.encryption = encryption;
198		this.status = status;
199		this.type = type;
200		this.carbon = carbon;
201		this.remoteMsgId = remoteMsgId;
202		this.relativeFilePath = relativeFilePath;
203		this.serverMsgId = serverMsgId;
204		this.axolotlFingerprint = fingerprint;
205		this.read = read;
206		this.edits = Edit.fromJson(edited);
207		this.oob = oob;
208		this.errorMessage = errorMessage;
209		this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
210		this.markable = markable;
211		this.deleted = deleted;
212		this.bodyLanguage = bodyLanguage;
213	}
214
215	public static Message fromCursor(Cursor cursor, Conversation conversation) {
216		return new Message(conversation,
217				cursor.getString(cursor.getColumnIndex(UUID)),
218				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
219				fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
220				fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
221				cursor.getString(cursor.getColumnIndex(BODY)),
222				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
223				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
224				cursor.getInt(cursor.getColumnIndex(STATUS)),
225				cursor.getInt(cursor.getColumnIndex(TYPE)),
226				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
227				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
228				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
229				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
230				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
231				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
232				cursor.getString(cursor.getColumnIndex(EDITED)),
233				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
234				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
235				ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
236				cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
237				cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
238				cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
239		);
240	}
241
242	private static Jid fromString(String value) {
243		try {
244			if (value != null) {
245				return Jid.of(value);
246			}
247		} catch (IllegalArgumentException e) {
248			return null;
249		}
250		return null;
251	}
252
253	public static Message createStatusMessage(Conversation conversation, String body) {
254		final Message message = new Message(conversation);
255		message.setType(Message.TYPE_STATUS);
256		message.setStatus(Message.STATUS_RECEIVED);
257		message.body = body;
258		return message;
259	}
260
261	public static Message createLoadMoreMessage(Conversation conversation) {
262		final Message message = new Message(conversation);
263		message.setType(Message.TYPE_STATUS);
264		message.body = "LOAD_MORE";
265		return message;
266	}
267
268	@Override
269	public ContentValues getContentValues() {
270		ContentValues values = new ContentValues();
271		values.put(UUID, uuid);
272		values.put(CONVERSATION, conversationUuid);
273		if (counterpart == null) {
274			values.putNull(COUNTERPART);
275		} else {
276			values.put(COUNTERPART, counterpart.toString());
277		}
278		if (trueCounterpart == null) {
279			values.putNull(TRUE_COUNTERPART);
280		} else {
281			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
282		}
283		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
284		values.put(TIME_SENT, timeSent);
285		values.put(ENCRYPTION, encryption);
286		values.put(STATUS, status);
287		values.put(TYPE, type);
288		values.put(CARBON, carbon ? 1 : 0);
289		values.put(REMOTE_MSG_ID, remoteMsgId);
290		values.put(RELATIVE_FILE_PATH, relativeFilePath);
291		values.put(SERVER_MSG_ID, serverMsgId);
292		values.put(FINGERPRINT, axolotlFingerprint);
293		values.put(READ, read ? 1 : 0);
294		try {
295			values.put(EDITED, Edit.toJson(edits));
296		} catch (JSONException e) {
297			Log.e(Config.LOGTAG,"error persisting json for edits",e);
298		}
299		values.put(OOB, oob ? 1 : 0);
300		values.put(ERROR_MESSAGE, errorMessage);
301		values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
302		values.put(MARKABLE, markable ? 1 : 0);
303		values.put(DELETED, deleted ? 1 : 0);
304		values.put(BODY_LANGUAGE, bodyLanguage);
305		return values;
306	}
307
308	public String getConversationUuid() {
309		return conversationUuid;
310	}
311
312	public Conversational getConversation() {
313		return this.conversation;
314	}
315
316	public Jid getCounterpart() {
317		return counterpart;
318	}
319
320	public void setCounterpart(final Jid counterpart) {
321		this.counterpart = counterpart;
322	}
323
324	public Contact getContact() {
325		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
326			return this.conversation.getContact();
327		} else {
328			if (this.trueCounterpart == null) {
329				return null;
330			} else {
331				return this.conversation.getAccount().getRoster()
332						.getContactFromContactList(this.trueCounterpart);
333			}
334		}
335	}
336
337	public String getBody() {
338		return body;
339	}
340
341	public synchronized void setBody(String body) {
342		if (body == null) {
343			throw new Error("You should not set the message body to null");
344		}
345		this.body = body;
346		this.isGeoUri = null;
347		this.isEmojisOnly = null;
348		this.treatAsDownloadable = null;
349		this.fileParams = null;
350	}
351
352	public void setMucUser(MucOptions.User user) {
353		this.user = new WeakReference<>(user);
354	}
355
356	public boolean sameMucUser(Message otherMessage) {
357		final MucOptions.User thisUser = this.user == null ? null : this.user.get();
358		final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
359		return thisUser != null && thisUser == otherUser;
360	}
361
362	public String getErrorMessage() {
363		return errorMessage;
364	}
365
366	public boolean setErrorMessage(String message) {
367		boolean changed = (message != null && !message.equals(errorMessage))
368				|| (message == null && errorMessage != null);
369		this.errorMessage = message;
370		return changed;
371	}
372
373	public long getTimeSent() {
374		return timeSent;
375	}
376
377	public int getEncryption() {
378		return encryption;
379	}
380
381	public void setEncryption(int encryption) {
382		this.encryption = encryption;
383	}
384
385	public int getStatus() {
386		return status;
387	}
388
389	public void setStatus(int status) {
390		this.status = status;
391	}
392
393	public String getRelativeFilePath() {
394		return this.relativeFilePath;
395	}
396
397	public void setRelativeFilePath(String path) {
398		this.relativeFilePath = path;
399	}
400
401	public String getRemoteMsgId() {
402		return this.remoteMsgId;
403	}
404
405	public void setRemoteMsgId(String id) {
406		this.remoteMsgId = id;
407	}
408
409	public String getServerMsgId() {
410		return this.serverMsgId;
411	}
412
413	public void setServerMsgId(String id) {
414		this.serverMsgId = id;
415	}
416
417	public boolean isRead() {
418		return this.read;
419	}
420
421	public boolean isDeleted() {
422		return this.deleted;
423	}
424
425	public void setDeleted(boolean deleted) {
426		this.deleted = deleted;
427	}
428
429	public void markRead() {
430		this.read = true;
431	}
432
433	public void markUnread() {
434		this.read = false;
435	}
436
437	public void setTime(long time) {
438		this.timeSent = time;
439	}
440
441	public String getEncryptedBody() {
442		return this.encryptedBody;
443	}
444
445	public void setEncryptedBody(String body) {
446		this.encryptedBody = body;
447	}
448
449	public int getType() {
450		return this.type;
451	}
452
453	public void setType(int type) {
454		this.type = type;
455	}
456
457	public boolean isCarbon() {
458		return carbon;
459	}
460
461	public void setCarbon(boolean carbon) {
462		this.carbon = carbon;
463	}
464
465	public void putEdited(String edited, String serverMsgId) {
466		final Edit edit = new Edit(edited, serverMsgId);
467		if (this.edits.size() < 128 && !this.edits.contains(edit)) {
468			this.edits.add(edit);
469		}
470	}
471
472	boolean remoteMsgIdMatchInEdit(String id) {
473		for(Edit edit : this.edits) {
474			if (id.equals(edit.getEditedId())) {
475				return true;
476			}
477		}
478		return false;
479	}
480
481	public String getBodyLanguage() {
482		return this.bodyLanguage;
483	}
484
485	public void setBodyLanguage(String language) {
486		this.bodyLanguage = language;
487	}
488
489	public boolean edited() {
490		return this.edits.size() > 0;
491	}
492
493	public void setTrueCounterpart(Jid trueCounterpart) {
494		this.trueCounterpart = trueCounterpart;
495	}
496
497	public Jid getTrueCounterpart() {
498		return this.trueCounterpart;
499	}
500
501	public Transferable getTransferable() {
502		return this.transferable;
503	}
504
505	public synchronized void setTransferable(Transferable transferable) {
506		this.fileParams = null;
507		this.transferable = transferable;
508	}
509
510	public boolean addReadByMarker(ReadByMarker readByMarker) {
511		if (readByMarker.getRealJid() != null) {
512			if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
513				return false;
514			}
515		} else if (readByMarker.getFullJid() != null) {
516			if (readByMarker.getFullJid().equals(counterpart)) {
517				return false;
518			}
519		}
520		if (this.readByMarkers.add(readByMarker)) {
521			if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
522				Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
523				while (iterator.hasNext()) {
524					ReadByMarker marker = iterator.next();
525					if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
526						iterator.remove();
527					}
528				}
529			}
530			return true;
531		} else {
532			return false;
533		}
534	}
535
536	public Set<ReadByMarker> getReadByMarkers() {
537		return Collections.unmodifiableSet(this.readByMarkers);
538	}
539
540	boolean similar(Message message) {
541		if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
542			return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
543		} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
544			return true;
545		} else if (this.body == null || this.counterpart == null) {
546			return false;
547		} else {
548			String body, otherBody;
549			if (this.hasFileOnRemoteHost()) {
550				body = getFileParams().url.toString();
551				otherBody = message.body == null ? null : message.body.trim();
552			} else {
553				body = this.body;
554				otherBody = message.body;
555			}
556			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
557			if (message.getRemoteMsgId() != null) {
558				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
559				if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
560					return true;
561				}
562				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
563						&& matchingCounterpart
564						&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
565			} else {
566				return this.remoteMsgId == null
567						&& matchingCounterpart
568						&& body.equals(otherBody)
569						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
570			}
571		}
572	}
573
574	public Message next() {
575		if (this.conversation instanceof Conversation) {
576			final Conversation conversation = (Conversation) this.conversation;
577			synchronized (conversation.messages) {
578				if (this.mNextMessage == null) {
579					int index = conversation.messages.indexOf(this);
580					if (index < 0 || index >= conversation.messages.size() - 1) {
581						this.mNextMessage = null;
582					} else {
583						this.mNextMessage = conversation.messages.get(index + 1);
584					}
585				}
586				return this.mNextMessage;
587			}
588		} else {
589			throw new AssertionError("Calling next should be disabled for stubs");
590		}
591	}
592
593	public Message prev() {
594		if (this.conversation instanceof Conversation) {
595			final Conversation conversation = (Conversation) this.conversation;
596			synchronized (conversation.messages) {
597				if (this.mPreviousMessage == null) {
598					int index = conversation.messages.indexOf(this);
599					if (index <= 0 || index > conversation.messages.size()) {
600						this.mPreviousMessage = null;
601					} else {
602						this.mPreviousMessage = conversation.messages.get(index - 1);
603					}
604				}
605			}
606			return this.mPreviousMessage;
607		} else {
608			throw new AssertionError("Calling prev should be disabled for stubs");
609		}
610	}
611
612	public boolean isLastCorrectableMessage() {
613		Message next = next();
614		while (next != null) {
615			if (next.isCorrectable()) {
616				return false;
617			}
618			next = next.next();
619		}
620		return isCorrectable();
621	}
622
623	private boolean isCorrectable() {
624		return getStatus() != STATUS_RECEIVED && !isCarbon();
625	}
626
627	public boolean mergeable(final Message message) {
628		return message != null &&
629				(message.getType() == Message.TYPE_TEXT &&
630						this.getTransferable() == null &&
631						message.getTransferable() == null &&
632						message.getEncryption() != Message.ENCRYPTION_PGP &&
633						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
634						this.getType() == message.getType() &&
635						//this.getStatus() == message.getStatus() &&
636						isStatusMergeable(this.getStatus(), message.getStatus()) &&
637						this.getEncryption() == message.getEncryption() &&
638						this.getCounterpart() != null &&
639						this.getCounterpart().equals(message.getCounterpart()) &&
640						this.edited() == message.edited() &&
641						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
642						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
643						!message.isGeoUri() &&
644						!this.isGeoUri() &&
645						!message.isOOb() &&
646						!this.isOOb() &&
647						!message.treatAsDownloadable() &&
648						!this.treatAsDownloadable() &&
649						!message.hasMeCommand() &&
650						!this.hasMeCommand() &&
651						!this.bodyIsOnlyEmojis() &&
652						!message.bodyIsOnlyEmojis() &&
653						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
654						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
655						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
656						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
657				);
658	}
659
660	private static boolean isStatusMergeable(int a, int b) {
661		return a == b || (
662				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
663						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
664						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
665						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
666						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
667		);
668	}
669
670	public void setCounterparts(List<MucOptions.User> counterparts) {
671		this.counterparts = counterparts;
672	}
673
674	public List<MucOptions.User> getCounterparts() {
675		return this.counterparts;
676	}
677
678	@Override
679	public int getAvatarBackgroundColor() {
680		if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
681			return Color.TRANSPARENT;
682		} else {
683			return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
684		}
685	}
686
687	public boolean isOOb() {
688		return oob;
689	}
690
691	public static class MergeSeparator {
692	}
693
694	public SpannableStringBuilder getMergedBody() {
695		SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
696		Message current = this;
697		while (current.mergeable(current.next())) {
698			current = current.next();
699			if (current == null) {
700				break;
701			}
702			body.append("\n\n");
703			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
704					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
705			body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
706		}
707		return body;
708	}
709
710	public boolean hasMeCommand() {
711		return this.body.trim().startsWith(ME_COMMAND);
712	}
713
714	public int getMergedStatus() {
715		int status = this.status;
716		Message current = this;
717		while (current.mergeable(current.next())) {
718			current = current.next();
719			if (current == null) {
720				break;
721			}
722			status = current.status;
723		}
724		return status;
725	}
726
727	public long getMergedTimeSent() {
728		long time = this.timeSent;
729		Message current = this;
730		while (current.mergeable(current.next())) {
731			current = current.next();
732			if (current == null) {
733				break;
734			}
735			time = current.timeSent;
736		}
737		return time;
738	}
739
740	public boolean wasMergedIntoPrevious() {
741		Message prev = this.prev();
742		return prev != null && prev.mergeable(this);
743	}
744
745	public boolean trusted() {
746		Contact contact = this.getContact();
747		return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
748	}
749
750	public boolean fixCounterpart() {
751		final Presences presences = conversation.getContact().getPresences();
752		if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
753			return true;
754		} else if (presences.size() >= 1) {
755			counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
756			return true;
757		} else {
758			counterpart = null;
759			return false;
760		}
761	}
762
763	public void setUuid(String uuid) {
764		this.uuid = uuid;
765	}
766
767	public String getEditedId() {
768		if (edits.size() > 0) {
769			return edits.get(edits.size() - 1).getEditedId();
770		} else {
771			throw new IllegalStateException("Attempting to store unedited message");
772		}
773	}
774
775	public String getEditedIdWireFormat() {
776		if (edits.size() > 0) {
777			return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
778		} else {
779			throw new IllegalStateException("Attempting to store unedited message");
780		}
781	}
782
783	public void setOob(boolean isOob) {
784		this.oob = isOob;
785	}
786
787	public String getMimeType() {
788		String extension;
789		if (relativeFilePath != null) {
790			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
791		} else {
792			try {
793				final URL url = new URL(body.split("\n")[0]);
794				extension = MimeUtils.extractRelevantExtension(url);
795			} catch (MalformedURLException e) {
796				return null;
797			}
798		}
799		return MimeUtils.guessMimeTypeFromExtension(extension);
800	}
801
802	public synchronized boolean treatAsDownloadable() {
803		if (treatAsDownloadable == null) {
804			treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
805		}
806		return treatAsDownloadable;
807	}
808
809	public synchronized boolean bodyIsOnlyEmojis() {
810		if (isEmojisOnly == null) {
811			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
812		}
813		return isEmojisOnly;
814	}
815
816	public synchronized boolean isGeoUri() {
817		if (isGeoUri == null) {
818			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
819		}
820		return isGeoUri;
821	}
822
823	public synchronized void resetFileParams() {
824		this.fileParams = null;
825	}
826
827	public synchronized FileParams getFileParams() {
828		if (fileParams == null) {
829			fileParams = new FileParams();
830			if (this.transferable != null) {
831				fileParams.size = this.transferable.getFileSize();
832			}
833			final String[] parts = body == null ? new String[0] : body.split("\\|");
834			switch (parts.length) {
835				case 1:
836					try {
837						fileParams.size = Long.parseLong(parts[0]);
838					} catch (NumberFormatException e) {
839						fileParams.url = parseUrl(parts[0]);
840					}
841					break;
842				case 5:
843					fileParams.runtime = parseInt(parts[4]);
844				case 4:
845					fileParams.width = parseInt(parts[2]);
846					fileParams.height = parseInt(parts[3]);
847				case 2:
848					fileParams.url = parseUrl(parts[0]);
849					fileParams.size = parseLong(parts[1]);
850					break;
851				case 3:
852					fileParams.size = parseLong(parts[0]);
853					fileParams.width = parseInt(parts[1]);
854					fileParams.height = parseInt(parts[2]);
855					break;
856			}
857		}
858		return fileParams;
859	}
860
861	private static long parseLong(String value) {
862		try {
863			return Long.parseLong(value);
864		} catch (NumberFormatException e) {
865			return 0;
866		}
867	}
868
869	private static int parseInt(String value) {
870		try {
871			return Integer.parseInt(value);
872		} catch (NumberFormatException e) {
873			return 0;
874		}
875	}
876
877	private static URL parseUrl(String value) {
878		try {
879			return new URL(value);
880		} catch (MalformedURLException e) {
881			return null;
882		}
883	}
884
885	public void untie() {
886		this.mNextMessage = null;
887		this.mPreviousMessage = null;
888	}
889
890	public boolean isPrivateMessage() {
891		return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
892	}
893
894	public boolean isFileOrImage() {
895		return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
896	}
897
898	public boolean hasFileOnRemoteHost() {
899		return isFileOrImage() && getFileParams().url != null;
900	}
901
902	public boolean needsUploading() {
903		return isFileOrImage() && getFileParams().url == null;
904	}
905
906	public class FileParams {
907		public URL url;
908		public long size = 0;
909		public int width = 0;
910		public int height = 0;
911		public int runtime = 0;
912	}
913
914	public void setFingerprint(String fingerprint) {
915		this.axolotlFingerprint = fingerprint;
916	}
917
918	public String getFingerprint() {
919		return axolotlFingerprint;
920	}
921
922	public boolean isTrusted() {
923		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
924		return s != null && s.isTrusted();
925	}
926
927	private int getPreviousEncryption() {
928		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
929			if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
930				continue;
931			}
932			return iterator.getEncryption();
933		}
934		return ENCRYPTION_NONE;
935	}
936
937	private int getNextEncryption() {
938		if (this.conversation instanceof Conversation) {
939			Conversation conversation = (Conversation) this.conversation;
940			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
941				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
942					continue;
943				}
944				return iterator.getEncryption();
945			}
946			return conversation.getNextEncryption();
947		} else {
948			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
949		}
950	}
951
952	public boolean isValidInSession() {
953		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
954		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
955
956		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
957				|| futureEncryption == ENCRYPTION_NONE
958				|| pastEncryption != futureEncryption;
959
960		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
961	}
962
963	private static int getCleanedEncryption(int encryption) {
964		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
965			return ENCRYPTION_PGP;
966		}
967		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
968			return ENCRYPTION_AXOLOTL;
969		}
970		return encryption;
971	}
972
973	public static boolean configurePrivateMessage(final Message message) {
974		return configurePrivateMessage(message, false);
975	}
976
977	public static boolean configurePrivateFileMessage(final Message message) {
978		return configurePrivateMessage(message, true);
979	}
980
981	private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
982		final Conversation conversation;
983		if (message.conversation instanceof Conversation) {
984			conversation = (Conversation) message.conversation;
985		} else {
986			return false;
987		}
988		if (conversation.getMode() == Conversation.MODE_MULTI) {
989			final Jid nextCounterpart = conversation.getNextCounterpart();
990			if (nextCounterpart != null) {
991				message.setCounterpart(nextCounterpart);
992				message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
993				message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
994				return true;
995			}
996		}
997		return false;
998	}
999}