Message.java

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