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