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