Message.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.text.SpannableStringBuilder;
  6
  7import java.lang.ref.WeakReference;
  8import java.net.MalformedURLException;
  9import java.net.URL;
 10import java.util.Collections;
 11import java.util.HashSet;
 12import java.util.Iterator;
 13import java.util.List;
 14import java.util.Set;
 15
 16import eu.siacs.conversations.Config;
 17import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 18import eu.siacs.conversations.http.AesGcmURLStreamHandler;
 19import eu.siacs.conversations.ui.adapter.MessageAdapter;
 20import eu.siacs.conversations.utils.CryptoHelper;
 21import eu.siacs.conversations.utils.Emoticons;
 22import eu.siacs.conversations.utils.GeoHelper;
 23import eu.siacs.conversations.utils.MimeUtils;
 24import eu.siacs.conversations.utils.UIHelper;
 25import rocks.xmpp.addr.Jid;
 26
 27public class Message extends AbstractEntity {
 28
 29	public static final String TABLENAME = "messages";
 30
 31	public static final int STATUS_RECEIVED = 0;
 32	public static final int STATUS_UNSEND = 1;
 33	public static final int STATUS_SEND = 2;
 34	public static final int STATUS_SEND_FAILED = 3;
 35	public static final int STATUS_WAITING = 5;
 36	public static final int STATUS_OFFERED = 6;
 37	public static final int STATUS_SEND_RECEIVED = 7;
 38	public static final int STATUS_SEND_DISPLAYED = 8;
 39
 40	public static final int ENCRYPTION_NONE = 0;
 41	public static final int ENCRYPTION_PGP = 1;
 42	public static final int ENCRYPTION_OTR = 2;
 43	public static final int ENCRYPTION_DECRYPTED = 3;
 44	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
 45	public static final int ENCRYPTION_AXOLOTL = 5;
 46	public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
 47
 48	public static final int TYPE_TEXT = 0;
 49	public static final int TYPE_IMAGE = 1;
 50	public static final int TYPE_FILE = 2;
 51	public static final int TYPE_STATUS = 3;
 52	public static final int TYPE_PRIVATE = 4;
 53
 54	public static final String CONVERSATION = "conversationUuid";
 55	public static final String COUNTERPART = "counterpart";
 56	public static final String TRUE_COUNTERPART = "trueCounterpart";
 57	public static final String BODY = "body";
 58	public static final String TIME_SENT = "timeSent";
 59	public static final String ENCRYPTION = "encryption";
 60	public static final String STATUS = "status";
 61	public static final String TYPE = "type";
 62	public static final String CARBON = "carbon";
 63	public static final String OOB = "oob";
 64	public static final String EDITED = "edited";
 65	public static final String REMOTE_MSG_ID = "remoteMsgId";
 66	public static final String SERVER_MSG_ID = "serverMsgId";
 67	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 68	public static final String FINGERPRINT = "axolotl_fingerprint";
 69	public static final String READ = "read";
 70	public static final String ERROR_MESSAGE = "errorMsg";
 71	public static final String READ_BY_MARKERS = "readByMarkers";
 72	public static final String MARKABLE = "markable";
 73	public static final String ME_COMMAND = "/me ";
 74
 75
 76	public boolean markable = false;
 77	protected String conversationUuid;
 78	protected Jid counterpart;
 79	protected Jid trueCounterpart;
 80	protected String body;
 81	protected String encryptedBody;
 82	protected long timeSent;
 83	protected int encryption;
 84	protected int status;
 85	protected int type;
 86	protected boolean carbon = false;
 87	protected boolean oob = false;
 88	protected String edited = null;
 89	protected String relativeFilePath;
 90	protected boolean read = true;
 91	protected String remoteMsgId = null;
 92	protected String serverMsgId = null;
 93	private final Conversational conversation;
 94	protected Transferable transferable = null;
 95	private Message mNextMessage = null;
 96	private Message mPreviousMessage = null;
 97	private String axolotlFingerprint = null;
 98	private String errorMessage = null;
 99	private Set<ReadByMarker> readByMarkers = new HashSet<>();
100
101	private Boolean isGeoUri = null;
102	private Boolean isEmojisOnly = null;
103	private Boolean treatAsDownloadable = null;
104	private FileParams fileParams = null;
105	private List<MucOptions.User> counterparts;
106	private WeakReference<MucOptions.User> user;
107
108	protected Message(Conversational conversation) {
109		this.conversation = conversation;
110	}
111
112	public Message(Conversational conversation, String body, int encryption) {
113		this(conversation, body, encryption, STATUS_UNSEND);
114	}
115
116	public Message(Conversational 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().asBareJid(),
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				false);
137	}
138
139	protected Message(final Conversational 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	                final boolean markable) {
146		this.conversation = conversation;
147		this.uuid = uuid;
148		this.conversationUuid = conversationUUid;
149		this.counterpart = counterpart;
150		this.trueCounterpart = trueCounterpart;
151		this.body = body == null ? "" : body;
152		this.timeSent = timeSent;
153		this.encryption = encryption;
154		this.status = status;
155		this.type = type;
156		this.carbon = carbon;
157		this.remoteMsgId = remoteMsgId;
158		this.relativeFilePath = relativeFilePath;
159		this.serverMsgId = serverMsgId;
160		this.axolotlFingerprint = fingerprint;
161		this.read = read;
162		this.edited = edited;
163		this.oob = oob;
164		this.errorMessage = errorMessage;
165		this.readByMarkers = readByMarkers == null ? new HashSet<ReadByMarker>() : readByMarkers;
166		this.markable = markable;
167	}
168
169	public static Message fromCursor(Cursor cursor, Conversation conversation) {
170		Jid jid;
171		try {
172			String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
173			if (value != null) {
174				jid = Jid.of(value);
175			} else {
176				jid = null;
177			}
178		} catch (IllegalArgumentException e) {
179			jid = null;
180		} catch (IllegalStateException e) {
181			return null; // message too long?
182		}
183		Jid trueCounterpart;
184		try {
185			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
186			if (value != null) {
187				trueCounterpart = Jid.of(value);
188			} else {
189				trueCounterpart = null;
190			}
191		} catch (IllegalArgumentException e) {
192			trueCounterpart = null;
193		}
194		return new Message(conversation,
195				cursor.getString(cursor.getColumnIndex(UUID)),
196				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
197				jid,
198				trueCounterpart,
199				cursor.getString(cursor.getColumnIndex(BODY)),
200				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
201				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
202				cursor.getInt(cursor.getColumnIndex(STATUS)),
203				cursor.getInt(cursor.getColumnIndex(TYPE)),
204				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
205				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
206				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
207				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
208				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
209				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
210				cursor.getString(cursor.getColumnIndex(EDITED)),
211				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
212				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
213				ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
214				cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0);
215	}
216
217	public static Message createStatusMessage(Conversation conversation, String body) {
218		final Message message = new Message(conversation);
219		message.setType(Message.TYPE_STATUS);
220		message.setStatus(Message.STATUS_RECEIVED);
221		message.body = body;
222		return message;
223	}
224
225	public static Message createLoadMoreMessage(Conversation conversation) {
226		final Message message = new Message(conversation);
227		message.setType(Message.TYPE_STATUS);
228		message.body = "LOAD_MORE";
229		return message;
230	}
231
232	@Override
233	public ContentValues getContentValues() {
234		ContentValues values = new ContentValues();
235		values.put(UUID, uuid);
236		values.put(CONVERSATION, conversationUuid);
237		if (counterpart == null) {
238			values.putNull(COUNTERPART);
239		} else {
240			values.put(COUNTERPART, counterpart.toString());
241		}
242		if (trueCounterpart == null) {
243			values.putNull(TRUE_COUNTERPART);
244		} else {
245			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
246		}
247		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
248		values.put(TIME_SENT, timeSent);
249		values.put(ENCRYPTION, encryption);
250		values.put(STATUS, status);
251		values.put(TYPE, type);
252		values.put(CARBON, carbon ? 1 : 0);
253		values.put(REMOTE_MSG_ID, remoteMsgId);
254		values.put(RELATIVE_FILE_PATH, relativeFilePath);
255		values.put(SERVER_MSG_ID, serverMsgId);
256		values.put(FINGERPRINT, axolotlFingerprint);
257		values.put(READ, read ? 1 : 0);
258		values.put(EDITED, edited);
259		values.put(OOB, oob ? 1 : 0);
260		values.put(ERROR_MESSAGE, errorMessage);
261		values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
262		values.put(MARKABLE, markable ? 1 : 0);
263		return values;
264	}
265
266	public String getConversationUuid() {
267		return conversationUuid;
268	}
269
270	public Conversational getConversation() {
271		return this.conversation;
272	}
273
274	public Jid getCounterpart() {
275		return counterpart;
276	}
277
278	public void setCounterpart(final Jid counterpart) {
279		this.counterpart = counterpart;
280	}
281
282	public Contact getContact() {
283		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
284			return this.conversation.getContact();
285		} else {
286			if (this.trueCounterpart == null) {
287				return null;
288			} else {
289				return this.conversation.getAccount().getRoster()
290						.getContactFromRoster(this.trueCounterpart);
291			}
292		}
293	}
294
295	public String getBody() {
296		return body;
297	}
298
299	public synchronized void setBody(String body) {
300		if (body == null) {
301			throw new Error("You should not set the message body to null");
302		}
303		this.body = body;
304		this.isGeoUri = null;
305		this.isEmojisOnly = null;
306		this.treatAsDownloadable = null;
307		this.fileParams = null;
308	}
309
310	public void setMucUser(MucOptions.User user) {
311		this.user = new WeakReference<>(user);
312	}
313
314	public boolean sameMucUser(Message otherMessage) {
315		final MucOptions.User thisUser = this.user == null ? null : this.user.get();
316		final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
317		return thisUser != null && thisUser == otherUser;
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().asBareJid().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		if (this.conversation instanceof Conversation) {
504			final Conversation conversation = (Conversation) this.conversation;
505			synchronized (conversation.messages) {
506				if (this.mNextMessage == null) {
507					int index = conversation.messages.indexOf(this);
508					if (index < 0 || index >= conversation.messages.size() - 1) {
509						this.mNextMessage = null;
510					} else {
511						this.mNextMessage = conversation.messages.get(index + 1);
512					}
513				}
514				return this.mNextMessage;
515			}
516		} else {
517			throw new AssertionError("Calling next should be disabled for stubs");
518		}
519	}
520
521	public Message prev() {
522		if (this.conversation instanceof Conversation) {
523			final Conversation conversation = (Conversation) this.conversation;
524			synchronized (conversation.messages) {
525				if (this.mPreviousMessage == null) {
526					int index = conversation.messages.indexOf(this);
527					if (index <= 0 || index > conversation.messages.size()) {
528						this.mPreviousMessage = null;
529					} else {
530						this.mPreviousMessage = conversation.messages.get(index - 1);
531					}
532				}
533			}
534			return this.mPreviousMessage;
535		} else {
536			throw new AssertionError("Calling prev should be disabled for stubs");
537		}
538	}
539
540	public boolean isLastCorrectableMessage() {
541		Message next = next();
542		while (next != null) {
543			if (next.isCorrectable()) {
544				return false;
545			}
546			next = next.next();
547		}
548		return isCorrectable();
549	}
550
551	private boolean isCorrectable() {
552		return getStatus() != STATUS_RECEIVED && !isCarbon();
553	}
554
555	public boolean mergeable(final Message message) {
556		return message != null &&
557				(message.getType() == Message.TYPE_TEXT &&
558						this.getTransferable() == null &&
559						message.getTransferable() == null &&
560						message.getEncryption() != Message.ENCRYPTION_PGP &&
561						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
562						this.getType() == message.getType() &&
563						//this.getStatus() == message.getStatus() &&
564						isStatusMergeable(this.getStatus(), message.getStatus()) &&
565						this.getEncryption() == message.getEncryption() &&
566						this.getCounterpart() != null &&
567						this.getCounterpart().equals(message.getCounterpart()) &&
568						this.edited() == message.edited() &&
569						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
570						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
571						!message.isGeoUri() &&
572						!this.isGeoUri() &&
573						!message.treatAsDownloadable() &&
574						!this.treatAsDownloadable() &&
575						!message.getBody().startsWith(ME_COMMAND) &&
576						!this.getBody().startsWith(ME_COMMAND) &&
577						!this.bodyIsOnlyEmojis() &&
578						!message.bodyIsOnlyEmojis() &&
579						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
580						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
581						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
582						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
583				);
584	}
585
586	private static boolean isStatusMergeable(int a, int b) {
587		return a == b || (
588				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
589						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
590						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
591						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
592						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
593		);
594	}
595
596	public void setCounterparts(List<MucOptions.User> counterparts) {
597		this.counterparts = counterparts;
598	}
599
600	public List<MucOptions.User> getCounterparts() {
601		return this.counterparts;
602	}
603
604	public static class MergeSeparator {
605	}
606
607	public SpannableStringBuilder getMergedBody() {
608		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
609		Message current = this;
610		while (current.mergeable(current.next())) {
611			current = current.next();
612			if (current == null) {
613				break;
614			}
615			body.append("\n\n");
616			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
617					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
618			body.append(current.getBody().trim());
619		}
620		return body;
621	}
622
623	public boolean hasMeCommand() {
624		return this.body.trim().startsWith(ME_COMMAND);
625	}
626
627	public int getMergedStatus() {
628		int status = this.status;
629		Message current = this;
630		while (current.mergeable(current.next())) {
631			current = current.next();
632			if (current == null) {
633				break;
634			}
635			status = current.status;
636		}
637		return status;
638	}
639
640	public long getMergedTimeSent() {
641		long time = this.timeSent;
642		Message current = this;
643		while (current.mergeable(current.next())) {
644			current = current.next();
645			if (current == null) {
646				break;
647			}
648			time = current.timeSent;
649		}
650		return time;
651	}
652
653	public boolean wasMergedIntoPrevious() {
654		Message prev = this.prev();
655		return prev != null && prev.mergeable(this);
656	}
657
658	public boolean trusted() {
659		Contact contact = this.getContact();
660		return status > STATUS_RECEIVED || (contact != null && (contact.showInRoster() || contact.isSelf()));
661	}
662
663	public boolean fixCounterpart() {
664		Presences presences = conversation.getContact().getPresences();
665		if (counterpart != null && presences.has(counterpart.getResource())) {
666			return true;
667		} else if (presences.size() >= 1) {
668			try {
669				counterpart = Jid.of(conversation.getJid().getLocal(),
670						conversation.getJid().getDomain(),
671						presences.toResourceArray()[0]);
672				return true;
673			} catch (IllegalArgumentException e) {
674				counterpart = null;
675				return false;
676			}
677		} else {
678			counterpart = null;
679			return false;
680		}
681	}
682
683	public void setUuid(String uuid) {
684		this.uuid = uuid;
685	}
686
687	public String getEditedId() {
688		return edited;
689	}
690
691	public void setOob(boolean isOob) {
692		this.oob = isOob;
693	}
694
695	public String getMimeType() {
696		String extension;
697		if (relativeFilePath != null) {
698			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
699		} else {
700			try {
701				final URL url = new URL(body.split("\n")[0]);
702				extension = MimeUtils.extractRelevantExtension(url);
703			} catch (MalformedURLException e) {
704				return null;
705			}
706		}
707		return MimeUtils.guessMimeTypeFromExtension(extension);
708	}
709
710	public synchronized boolean treatAsDownloadable() {
711		if (treatAsDownloadable == null) {
712			try {
713				final String[] lines = body.split("\n");
714				if (lines.length == 0) {
715					treatAsDownloadable = false;
716					return false;
717				}
718				for (String line : lines) {
719					if (line.contains("\\s+")) {
720						treatAsDownloadable = false;
721						return false;
722					}
723				}
724				final URL url = new URL(lines[0]);
725				final String ref = url.getRef();
726				final String protocol = url.getProtocol();
727				final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
728				final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
729				final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
730				final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
731				treatAsDownloadable = validAesGcm || validOob;
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		if (this.conversation instanceof Conversation) {
865			Conversation conversation = (Conversation) this.conversation;
866			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
867				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
868					continue;
869				}
870				return iterator.getEncryption();
871			}
872			return conversation.getNextEncryption();
873		} else {
874			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
875		}
876	}
877
878	public boolean isValidInSession() {
879		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
880		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
881
882		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
883				|| futureEncryption == ENCRYPTION_NONE
884				|| pastEncryption != futureEncryption;
885
886		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
887	}
888
889	private static int getCleanedEncryption(int encryption) {
890		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
891			return ENCRYPTION_PGP;
892		}
893		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
894			return ENCRYPTION_AXOLOTL;
895		}
896		return encryption;
897	}
898}