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