Message.java

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