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.List;
 16import java.util.Set;
 17
 18import eu.siacs.conversations.Config;
 19import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 20import eu.siacs.conversations.http.AesGcmURLStreamHandler;
 21import eu.siacs.conversations.ui.adapter.MessageAdapter;
 22import eu.siacs.conversations.utils.CryptoHelper;
 23import eu.siacs.conversations.utils.Emoticons;
 24import eu.siacs.conversations.utils.GeoHelper;
 25import eu.siacs.conversations.utils.MimeUtils;
 26import eu.siacs.conversations.utils.UIHelper;
 27import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 28import eu.siacs.conversations.xmpp.jid.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
 50	public static final int TYPE_TEXT = 0;
 51	public static final int TYPE_IMAGE = 1;
 52	public static final int TYPE_FILE = 2;
 53	public static final int TYPE_STATUS = 3;
 54	public static final int TYPE_PRIVATE = 4;
 55
 56	public static final String CONVERSATION = "conversationUuid";
 57	public static final String COUNTERPART = "counterpart";
 58	public static final String TRUE_COUNTERPART = "trueCounterpart";
 59	public static final String BODY = "body";
 60	public static final String TIME_SENT = "timeSent";
 61	public static final String ENCRYPTION = "encryption";
 62	public static final String STATUS = "status";
 63	public static final String TYPE = "type";
 64	public static final String CARBON = "carbon";
 65	public static final String OOB = "oob";
 66	public static final String EDITED = "edited";
 67	public static final String REMOTE_MSG_ID = "remoteMsgId";
 68	public static final String SERVER_MSG_ID = "serverMsgId";
 69	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 70	public static final String FINGERPRINT = "axolotl_fingerprint";
 71	public static final String READ = "read";
 72	public static final String ERROR_MESSAGE = "errorMsg";
 73	public static final String READ_BY_MARKERS = "readByMarkers";
 74	public static final String ME_COMMAND = "/me ";
 75
 76
 77	public boolean markable = false;
 78	protected String conversationUuid;
 79	protected Jid counterpart;
 80	protected Jid trueCounterpart;
 81	protected String body;
 82	protected String encryptedBody;
 83	protected long timeSent;
 84	protected int encryption;
 85	protected int status;
 86	protected int type;
 87	protected boolean carbon = false;
 88	protected boolean oob = false;
 89	protected String edited = null;
 90	protected String relativeFilePath;
 91	protected boolean read = true;
 92	protected String remoteMsgId = null;
 93	protected String serverMsgId = null;
 94	private final Conversation conversation;
 95	protected Transferable transferable = null;
 96	private Message mNextMessage = null;
 97	private Message mPreviousMessage = null;
 98	private String axolotlFingerprint = null;
 99	private String errorMessage = null;
100	protected Set<ReadByMarker> readByMarkers = new HashSet<>();
101
102	private Boolean isGeoUri = null;
103	private Boolean isEmojisOnly = null;
104	private Boolean treatAsDownloadable = null;
105	private FileParams fileParams = null;
106	private List<MucOptions.User> counterparts;
107
108	private Message(Conversation conversation) {
109		this.conversation = conversation;
110	}
111
112	public Message(Conversation conversation, String body, int encryption) {
113		this(conversation, body, encryption, STATUS_UNSEND);
114	}
115
116	public Message(Conversation conversation, String body, int encryption, int status) {
117		this(conversation, java.util.UUID.randomUUID().toString(),
118				conversation.getUuid(),
119				conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
120				null,
121				body,
122				System.currentTimeMillis(),
123				encryption,
124				status,
125				TYPE_TEXT,
126				false,
127				null,
128				null,
129				null,
130				null,
131				true,
132				null,
133				false,
134				null,
135				null);
136	}
137
138	private Message(final Conversation conversation, final String uuid, final String conversationUUid, final Jid counterpart,
139					final Jid trueCounterpart, final String body, final long timeSent,
140					final int encryption, final int status, final int type, final boolean carbon,
141					final String remoteMsgId, final String relativeFilePath,
142					final String serverMsgId, final String fingerprint, final boolean read,
143					final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers) {
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 = new HashSet<>();
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.fromString(value, true);
172			} else {
173				jid = null;
174			}
175		} catch (InvalidJidException 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.fromString(value, true);
185			} else {
186				trueCounterpart = null;
187			}
188		} catch (InvalidJidException 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	}
212
213	public static Message createStatusMessage(Conversation conversation, String body) {
214		final Message message = new Message(conversation);
215		message.setType(Message.TYPE_STATUS);
216		message.setStatus(Message.STATUS_RECEIVED);
217		message.body = body;
218		return message;
219	}
220
221	public static Message createLoadMoreMessage(Conversation conversation) {
222		final Message message = new Message(conversation);
223		message.setType(Message.TYPE_STATUS);
224		message.body = "LOAD_MORE";
225		return message;
226	}
227
228	public static Message createDateSeparator(Message message) {
229		final Message separator = new Message(message.getConversation());
230		separator.setType(Message.TYPE_STATUS);
231		separator.body = MessageAdapter.DATE_SEPARATOR_BODY;
232		separator.setTime(message.getTimeSent());
233		return separator;
234	}
235
236	@Override
237	public ContentValues getContentValues() {
238		ContentValues values = new ContentValues();
239		values.put(UUID, uuid);
240		values.put(CONVERSATION, conversationUuid);
241		if (counterpart == null) {
242			values.putNull(COUNTERPART);
243		} else {
244			values.put(COUNTERPART, counterpart.toPreppedString());
245		}
246		if (trueCounterpart == null) {
247			values.putNull(TRUE_COUNTERPART);
248		} else {
249			values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
250		}
251		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : body);
252		values.put(TIME_SENT, timeSent);
253		values.put(ENCRYPTION, encryption);
254		values.put(STATUS, status);
255		values.put(TYPE, type);
256		values.put(CARBON, carbon ? 1 : 0);
257		values.put(REMOTE_MSG_ID, remoteMsgId);
258		values.put(RELATIVE_FILE_PATH, relativeFilePath);
259		values.put(SERVER_MSG_ID, serverMsgId);
260		values.put(FINGERPRINT, axolotlFingerprint);
261		values.put(READ,read ? 1 : 0);
262		values.put(EDITED, edited);
263		values.put(OOB, oob ? 1 : 0);
264		values.put(ERROR_MESSAGE,errorMessage);
265		values.put(READ_BY_MARKERS,ReadByMarker.toJson(readByMarkers).toString());
266		return values;
267	}
268
269	public String getConversationUuid() {
270		return conversationUuid;
271	}
272
273	public Conversation getConversation() {
274		return this.conversation;
275	}
276
277	public Jid getCounterpart() {
278		return counterpart;
279	}
280
281	public void setCounterpart(final Jid counterpart) {
282		this.counterpart = counterpart;
283	}
284
285	public Contact getContact() {
286		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
287			return this.conversation.getContact();
288		} else {
289			if (this.trueCounterpart == null) {
290				return null;
291			} else {
292				return this.conversation.getAccount().getRoster()
293						.getContactFromRoster(this.trueCounterpart);
294			}
295		}
296	}
297
298	public String getBody() {
299		return body;
300	}
301
302	public synchronized void setBody(String body) {
303		if (body == null) {
304			throw new Error("You should not set the message body to null");
305		}
306		this.body = body;
307		this.isGeoUri = null;
308		this.isEmojisOnly = null;
309		this.treatAsDownloadable = null;
310		this.fileParams = null;
311	}
312
313	public String getErrorMessage() {
314		return errorMessage;
315	}
316
317	public boolean setErrorMessage(String message) {
318		boolean changed = (message != null && !message.equals(errorMessage))
319				|| (message == null && errorMessage != null);
320		this.errorMessage = message;
321		return changed;
322	}
323
324	public long getTimeSent() {
325		return timeSent;
326	}
327
328	public int getEncryption() {
329		return encryption;
330	}
331
332	public void setEncryption(int encryption) {
333		this.encryption = encryption;
334	}
335
336	public int getStatus() {
337		return status;
338	}
339
340	public void setStatus(int status) {
341		this.status = status;
342	}
343
344	public String getRelativeFilePath() {
345		return this.relativeFilePath;
346	}
347
348	public void setRelativeFilePath(String path) {
349		this.relativeFilePath = path;
350	}
351
352	public String getRemoteMsgId() {
353		return this.remoteMsgId;
354	}
355
356	public void setRemoteMsgId(String id) {
357		this.remoteMsgId = id;
358	}
359
360	public String getServerMsgId() {
361		return this.serverMsgId;
362	}
363
364	public void setServerMsgId(String id) {
365		this.serverMsgId = id;
366	}
367
368	public boolean isRead() {
369		return this.read;
370	}
371
372	public void markRead() {
373		this.read = true;
374	}
375
376	public void markUnread() {
377		this.read = false;
378	}
379
380	public void setTime(long time) {
381		this.timeSent = time;
382	}
383
384	public String getEncryptedBody() {
385		return this.encryptedBody;
386	}
387
388	public void setEncryptedBody(String body) {
389		this.encryptedBody = body;
390	}
391
392	public int getType() {
393		return this.type;
394	}
395
396	public void setType(int type) {
397		this.type = type;
398	}
399
400	public boolean isCarbon() {
401		return carbon;
402	}
403
404	public void setCarbon(boolean carbon) {
405		this.carbon = carbon;
406	}
407
408	public void setEdited(String edited) {
409		this.edited = edited;
410	}
411
412	public boolean edited() {
413		return this.edited != null;
414	}
415
416	public void setTrueCounterpart(Jid trueCounterpart) {
417		this.trueCounterpart = trueCounterpart;
418	}
419
420	public Jid getTrueCounterpart() {
421		return this.trueCounterpart;
422	}
423
424	public Transferable getTransferable() {
425		return this.transferable;
426	}
427
428	public synchronized void setTransferable(Transferable transferable) {
429		this.fileParams = null;
430		this.transferable = transferable;
431	}
432
433	public boolean addReadByMarker(ReadByMarker readByMarker) {
434		if (readByMarker.getRealJid() != null) {
435			if (readByMarker.getRealJid().toBareJid().equals(trueCounterpart)) {
436				Log.d(Config.LOGTAG,"trying to add read marker by "+readByMarker.getRealJid()+" to "+body);
437				return false;
438			}
439		} else if (readByMarker.getFullJid() != null) {
440			if (readByMarker.getFullJid().equals(counterpart)) {
441				Log.d(Config.LOGTAG,"trying to add read marker by "+readByMarker.getFullJid()+" to "+body);
442				return false;
443			}
444		}
445		return this.readByMarkers.add(readByMarker);
446	}
447
448	public Set<ReadByMarker> getReadByMarkers() {
449		return Collections.unmodifiableSet(this.readByMarkers);
450	}
451
452	public boolean similar(Message message) {
453		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
454			return this.serverMsgId.equals(message.getServerMsgId());
455		} else if (this.body == null || this.counterpart == null) {
456			return false;
457		} else {
458			String body, otherBody;
459			if (this.hasFileOnRemoteHost()) {
460				body = getFileParams().url.toString();
461				otherBody = message.body == null ? null : message.body.trim();
462			} else {
463				body = this.body;
464				otherBody = message.body;
465			}
466			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
467			if (message.getRemoteMsgId() != null) {
468				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
469				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
470					return true;
471				}
472				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
473						&& matchingCounterpart
474						&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
475			} else {
476				return this.remoteMsgId == null
477						&& matchingCounterpart
478						&& body.equals(otherBody)
479						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
480			}
481		}
482	}
483
484	public Message next() {
485		synchronized (this.conversation.messages) {
486			if (this.mNextMessage == null) {
487				int index = this.conversation.messages.indexOf(this);
488				if (index < 0 || index >= this.conversation.messages.size() - 1) {
489					this.mNextMessage = null;
490				} else {
491					this.mNextMessage = this.conversation.messages.get(index + 1);
492				}
493			}
494			return this.mNextMessage;
495		}
496	}
497
498	public Message prev() {
499		synchronized (this.conversation.messages) {
500			if (this.mPreviousMessage == null) {
501				int index = this.conversation.messages.indexOf(this);
502				if (index <= 0 || index > this.conversation.messages.size()) {
503					this.mPreviousMessage = null;
504				} else {
505					this.mPreviousMessage = this.conversation.messages.get(index - 1);
506				}
507			}
508			return this.mPreviousMessage;
509		}
510	}
511
512	public boolean isLastCorrectableMessage() {
513		Message next = next();
514		while(next != null) {
515			if (next.isCorrectable()) {
516				return false;
517			}
518			next = next.next();
519		}
520		return isCorrectable();
521	}
522
523	private boolean isCorrectable() {
524		return getStatus() != STATUS_RECEIVED && !isCarbon();
525	}
526
527	public boolean mergeable(final Message message) {
528		return message != null &&
529				(message.getType() == Message.TYPE_TEXT &&
530						this.getTransferable() == null &&
531						message.getTransferable() == null &&
532						message.getEncryption() != Message.ENCRYPTION_PGP &&
533						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
534						this.getType() == message.getType() &&
535						//this.getStatus() == message.getStatus() &&
536						isStatusMergeable(this.getStatus(), message.getStatus()) &&
537						this.getEncryption() == message.getEncryption() &&
538						this.getCounterpart() != null &&
539						this.getCounterpart().equals(message.getCounterpart()) &&
540						this.edited() == message.edited() &&
541						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
542						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
543						!message.isGeoUri()&&
544						!this.isGeoUri() &&
545						!message.treatAsDownloadable() &&
546						!this.treatAsDownloadable() &&
547						!message.getBody().startsWith(ME_COMMAND) &&
548						!this.getBody().startsWith(ME_COMMAND) &&
549						!this.bodyIsOnlyEmojis() &&
550						!message.bodyIsOnlyEmojis() &&
551						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
552						UIHelper.sameDay(message.getTimeSent(),this.getTimeSent()) &&
553						this.getReadByMarkers().equals(message.getReadByMarkers())
554				);
555	}
556
557	private static boolean isStatusMergeable(int a, int b) {
558		return a == b || (
559				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
560						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
561						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
562						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
563						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
564		);
565	}
566
567	public void setCounterparts(List<MucOptions.User> counterparts) {
568		this.counterparts = counterparts;
569	}
570
571	public List<MucOptions.User> getCounterparts() {
572		return this.counterparts;
573	}
574
575	public static class MergeSeparator {}
576
577	public SpannableStringBuilder getMergedBody() {
578		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
579		Message current = this;
580		while (current.mergeable(current.next())) {
581			current = current.next();
582			if (current == null) {
583				break;
584			}
585			body.append("\n\n");
586			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
587					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
588			body.append(current.getBody().trim());
589		}
590		return body;
591	}
592
593	public boolean hasMeCommand() {
594		return this.body.trim().startsWith(ME_COMMAND);
595	}
596
597	public int getMergedStatus() {
598		int status = this.status;
599		Message current = this;
600		while(current.mergeable(current.next())) {
601			current = current.next();
602			if (current == null) {
603				break;
604			}
605			status = current.status;
606		}
607		return status;
608	}
609
610	public long getMergedTimeSent() {
611		long time = this.timeSent;
612		Message current = this;
613		while(current.mergeable(current.next())) {
614			current = current.next();
615			if (current == null) {
616				break;
617			}
618			time = current.timeSent;
619		}
620		return time;
621	}
622
623	public boolean wasMergedIntoPrevious() {
624		Message prev = this.prev();
625		return prev != null && prev.mergeable(this);
626	}
627
628	public boolean trusted() {
629		Contact contact = this.getContact();
630		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
631	}
632
633	public boolean fixCounterpart() {
634		Presences presences = conversation.getContact().getPresences();
635		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
636			return true;
637		} else if (presences.size() >= 1) {
638			try {
639				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
640						conversation.getJid().getDomainpart(),
641						presences.toResourceArray()[0]);
642				return true;
643			} catch (InvalidJidException e) {
644				counterpart = null;
645				return false;
646			}
647		} else {
648			counterpart = null;
649			return false;
650		}
651	}
652
653	public void setUuid(String uuid) {
654		this.uuid = uuid;
655	}
656
657	public String getEditedId() {
658		return edited;
659	}
660
661	public void setOob(boolean isOob) {
662		this.oob = isOob;
663	}
664
665	private static String extractRelevantExtension(URL url) {
666		String path = url.getPath();
667		return extractRelevantExtension(path);
668	}
669
670	private static String extractRelevantExtension(String path) {
671		if (path == null || path.isEmpty()) {
672			return null;
673		}
674
675		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
676		int dotPosition = filename.lastIndexOf(".");
677
678		if (dotPosition != -1) {
679			String extension = filename.substring(dotPosition + 1);
680			// we want the real file extension, not the crypto one
681			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
682				return extractRelevantExtension(filename.substring(0,dotPosition));
683			} else {
684				return extension;
685			}
686		}
687		return null;
688	}
689
690	public String getMimeType() {
691		if (relativeFilePath != null) {
692			int start = relativeFilePath.lastIndexOf('.') + 1;
693			if (start < relativeFilePath.length()) {
694				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
695			} else {
696				return null;
697			}
698		} else {
699			try {
700				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
701			} catch (MalformedURLException e) {
702				return null;
703			}
704		}
705	}
706
707	public synchronized boolean treatAsDownloadable() {
708		if (treatAsDownloadable == null) {
709			if (body.trim().contains(" ")) {
710				treatAsDownloadable = false;
711			}
712			try {
713				final URL url = new URL(body);
714				final String ref = url.getRef();
715				final String protocol = url.getProtocol();
716				final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
717				treatAsDownloadable = (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
718						|| (("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted));
719
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}