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						!this.conversation.getJid().toBareJid().toString().equals(Config.BUG_REPORTS)
573				);
574	}
575
576	private static boolean isStatusMergeable(int a, int b) {
577		return a == b || (
578				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
579						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
580						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
581						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
582						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
583		);
584	}
585
586	public void setCounterparts(List<MucOptions.User> counterparts) {
587		this.counterparts = counterparts;
588	}
589
590	public List<MucOptions.User> getCounterparts() {
591		return this.counterparts;
592	}
593
594	public static class MergeSeparator {}
595
596	public SpannableStringBuilder getMergedBody() {
597		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
598		Message current = this;
599		while (current.mergeable(current.next())) {
600			current = current.next();
601			if (current == null) {
602				break;
603			}
604			body.append("\n\n");
605			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
606					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
607			body.append(current.getBody().trim());
608		}
609		return body;
610	}
611
612	public boolean hasMeCommand() {
613		return this.body.trim().startsWith(ME_COMMAND);
614	}
615
616	public int getMergedStatus() {
617		int status = this.status;
618		Message current = this;
619		while(current.mergeable(current.next())) {
620			current = current.next();
621			if (current == null) {
622				break;
623			}
624			status = current.status;
625		}
626		return status;
627	}
628
629	public long getMergedTimeSent() {
630		long time = this.timeSent;
631		Message current = this;
632		while(current.mergeable(current.next())) {
633			current = current.next();
634			if (current == null) {
635				break;
636			}
637			time = current.timeSent;
638		}
639		return time;
640	}
641
642	public boolean wasMergedIntoPrevious() {
643		Message prev = this.prev();
644		return prev != null && prev.mergeable(this);
645	}
646
647	public boolean trusted() {
648		Contact contact = this.getContact();
649		return status > STATUS_RECEIVED || (contact != null && (contact.mutualPresenceSubscription() || contact.isSelf()));
650	}
651
652	public boolean fixCounterpart() {
653		Presences presences = conversation.getContact().getPresences();
654		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
655			return true;
656		} else if (presences.size() >= 1) {
657			try {
658				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
659						conversation.getJid().getDomainpart(),
660						presences.toResourceArray()[0]);
661				return true;
662			} catch (InvalidJidException e) {
663				counterpart = null;
664				return false;
665			}
666		} else {
667			counterpart = null;
668			return false;
669		}
670	}
671
672	public void setUuid(String uuid) {
673		this.uuid = uuid;
674	}
675
676	public String getEditedId() {
677		return edited;
678	}
679
680	public void setOob(boolean isOob) {
681		this.oob = isOob;
682	}
683
684	public String getMimeType() {
685		String extension;
686		if (relativeFilePath != null) {
687			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
688		} else {
689			try {
690				final URL url = new URL(body.split("\n")[0]);
691				extension = MimeUtils.extractRelevantExtension(url);
692			} catch (MalformedURLException e) {
693				return null;
694			}
695		}
696		return MimeUtils.guessMimeTypeFromExtension(extension);
697	}
698
699	public synchronized boolean treatAsDownloadable() {
700		if (treatAsDownloadable == null) {
701			try {
702				final String[] lines = body.split("\n");
703				if (lines.length ==0) {
704					treatAsDownloadable = false;
705					return false;
706				}
707				for(String line : lines) {
708					if (line.contains("\\s+")) {
709						treatAsDownloadable = false;
710						return false;
711					}
712				}
713				final URL url = new URL(lines[0]);
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				final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
718				final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
719				final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
720				treatAsDownloadable = validAesGcm || validOob;
721			} catch (MalformedURLException e) {
722				treatAsDownloadable = false;
723			}
724		}
725		return treatAsDownloadable;
726	}
727
728	public synchronized boolean bodyIsOnlyEmojis() {
729		if (isEmojisOnly == null) {
730			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s",""));
731		}
732		return isEmojisOnly;
733	}
734
735	public synchronized boolean isGeoUri() {
736		if (isGeoUri == null) {
737			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
738		}
739		return isGeoUri;
740	}
741
742	public synchronized void resetFileParams() {
743		this.fileParams = null;
744	}
745
746	public synchronized FileParams getFileParams() {
747		if (fileParams == null) {
748			fileParams = new FileParams();
749			if (this.transferable != null) {
750				fileParams.size = this.transferable.getFileSize();
751			}
752			String parts[] = body == null ? new String[0] : body.split("\\|");
753			switch (parts.length) {
754				case 1:
755					try {
756						fileParams.size = Long.parseLong(parts[0]);
757					} catch (NumberFormatException e) {
758						fileParams.url = parseUrl(parts[0]);
759					}
760					break;
761				case 5:
762					fileParams.runtime = parseInt(parts[4]);
763				case 4:
764					fileParams.width = parseInt(parts[2]);
765					fileParams.height = parseInt(parts[3]);
766				case 2:
767					fileParams.url = parseUrl(parts[0]);
768					fileParams.size = parseLong(parts[1]);
769					break;
770				case 3:
771					fileParams.size = parseLong(parts[0]);
772					fileParams.width = parseInt(parts[1]);
773					fileParams.height = parseInt(parts[2]);
774					break;
775			}
776		}
777		return fileParams;
778	}
779
780	private static long parseLong(String value) {
781		try {
782			return Long.parseLong(value);
783		} catch (NumberFormatException e) {
784			return 0;
785		}
786	}
787
788	private static int parseInt(String value) {
789		try {
790			return Integer.parseInt(value);
791		} catch (NumberFormatException e) {
792			return 0;
793		}
794	}
795
796	private static URL parseUrl(String value) {
797		try {
798			return new URL(value);
799		} catch (MalformedURLException e) {
800			return null;
801		}
802	}
803
804	public void untie() {
805		this.mNextMessage = null;
806		this.mPreviousMessage = null;
807	}
808
809	public boolean isFileOrImage() {
810		return type == TYPE_FILE || type == TYPE_IMAGE;
811	}
812
813	public boolean hasFileOnRemoteHost() {
814		return isFileOrImage() && getFileParams().url != null;
815	}
816
817	public boolean needsUploading() {
818		return isFileOrImage() && getFileParams().url == null;
819	}
820
821	public class FileParams {
822		public URL url;
823		public long size = 0;
824		public int width = 0;
825		public int height = 0;
826		public int runtime = 0;
827	}
828
829	public void setFingerprint(String fingerprint) {
830		this.axolotlFingerprint = fingerprint;
831	}
832
833	public String getFingerprint() {
834		return axolotlFingerprint;
835	}
836
837	public boolean isTrusted() {
838		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
839		return s != null && s.isTrusted();
840	}
841
842	private  int getPreviousEncryption() {
843		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
844			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
845				continue;
846			}
847			return iterator.getEncryption();
848		}
849		return ENCRYPTION_NONE;
850	}
851
852	private int getNextEncryption() {
853		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
854			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
855				continue;
856			}
857			return iterator.getEncryption();
858		}
859		return conversation.getNextEncryption();
860	}
861
862	public boolean isValidInSession() {
863		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
864		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
865
866		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
867				|| futureEncryption == ENCRYPTION_NONE
868				|| pastEncryption != futureEncryption;
869
870		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
871	}
872
873	private static int getCleanedEncryption(int encryption) {
874		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
875			return ENCRYPTION_PGP;
876		}
877		return encryption;
878	}
879}