Message.java

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