Message.java

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