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