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	private 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	public static Message createDateSeparator(Message message) {
233		final Message separator = new Message(message.getConversation());
234		separator.setType(Message.TYPE_STATUS);
235		separator.body = MessageAdapter.DATE_SEPARATOR_BODY;
236		separator.setTime(message.getTimeSent());
237		return separator;
238	}
239
240	@Override
241	public ContentValues getContentValues() {
242		ContentValues values = new ContentValues();
243		values.put(UUID, uuid);
244		values.put(CONVERSATION, conversationUuid);
245		if (counterpart == null) {
246			values.putNull(COUNTERPART);
247		} else {
248			values.put(COUNTERPART, counterpart.toString());
249		}
250		if (trueCounterpart == null) {
251			values.putNull(TRUE_COUNTERPART);
252		} else {
253			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
254		}
255		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
256		values.put(TIME_SENT, timeSent);
257		values.put(ENCRYPTION, encryption);
258		values.put(STATUS, status);
259		values.put(TYPE, type);
260		values.put(CARBON, carbon ? 1 : 0);
261		values.put(REMOTE_MSG_ID, remoteMsgId);
262		values.put(RELATIVE_FILE_PATH, relativeFilePath);
263		values.put(SERVER_MSG_ID, serverMsgId);
264		values.put(FINGERPRINT, axolotlFingerprint);
265		values.put(READ, read ? 1 : 0);
266		values.put(EDITED, edited);
267		values.put(OOB, oob ? 1 : 0);
268		values.put(ERROR_MESSAGE, errorMessage);
269		values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
270		values.put(MARKABLE, markable ? 1 : 0);
271		return values;
272	}
273
274	public String getConversationUuid() {
275		return conversationUuid;
276	}
277
278	public Conversational getConversation() {
279		return this.conversation;
280	}
281
282	public Jid getCounterpart() {
283		return counterpart;
284	}
285
286	public void setCounterpart(final Jid counterpart) {
287		this.counterpart = counterpart;
288	}
289
290	public Contact getContact() {
291		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
292			return this.conversation.getContact();
293		} else {
294			if (this.trueCounterpart == null) {
295				return null;
296			} else {
297				return this.conversation.getAccount().getRoster()
298						.getContactFromRoster(this.trueCounterpart);
299			}
300		}
301	}
302
303	public String getBody() {
304		return body;
305	}
306
307	public synchronized void setBody(String body) {
308		if (body == null) {
309			throw new Error("You should not set the message body to null");
310		}
311		this.body = body;
312		this.isGeoUri = null;
313		this.isEmojisOnly = null;
314		this.treatAsDownloadable = null;
315		this.fileParams = null;
316	}
317
318	public void setMucUser(MucOptions.User user) {
319		this.user = new WeakReference<>(user);
320	}
321
322	public boolean sameMucUser(Message otherMessage) {
323		final MucOptions.User thisUser = this.user == null ? null : this.user.get();
324		final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
325		return thisUser != null && thisUser == otherUser;
326	}
327
328	public String getErrorMessage() {
329		return errorMessage;
330	}
331
332	public boolean setErrorMessage(String message) {
333		boolean changed = (message != null && !message.equals(errorMessage))
334				|| (message == null && errorMessage != null);
335		this.errorMessage = message;
336		return changed;
337	}
338
339	public long getTimeSent() {
340		return timeSent;
341	}
342
343	public int getEncryption() {
344		return encryption;
345	}
346
347	public void setEncryption(int encryption) {
348		this.encryption = encryption;
349	}
350
351	public int getStatus() {
352		return status;
353	}
354
355	public void setStatus(int status) {
356		this.status = status;
357	}
358
359	public String getRelativeFilePath() {
360		return this.relativeFilePath;
361	}
362
363	public void setRelativeFilePath(String path) {
364		this.relativeFilePath = path;
365	}
366
367	public String getRemoteMsgId() {
368		return this.remoteMsgId;
369	}
370
371	public void setRemoteMsgId(String id) {
372		this.remoteMsgId = id;
373	}
374
375	public String getServerMsgId() {
376		return this.serverMsgId;
377	}
378
379	public void setServerMsgId(String id) {
380		this.serverMsgId = id;
381	}
382
383	public boolean isRead() {
384		return this.read;
385	}
386
387	public void markRead() {
388		this.read = true;
389	}
390
391	public void markUnread() {
392		this.read = false;
393	}
394
395	public void setTime(long time) {
396		this.timeSent = time;
397	}
398
399	public String getEncryptedBody() {
400		return this.encryptedBody;
401	}
402
403	public void setEncryptedBody(String body) {
404		this.encryptedBody = body;
405	}
406
407	public int getType() {
408		return this.type;
409	}
410
411	public void setType(int type) {
412		this.type = type;
413	}
414
415	public boolean isCarbon() {
416		return carbon;
417	}
418
419	public void setCarbon(boolean carbon) {
420		this.carbon = carbon;
421	}
422
423	public void setEdited(String edited) {
424		this.edited = edited;
425	}
426
427	public boolean edited() {
428		return this.edited != null;
429	}
430
431	public void setTrueCounterpart(Jid trueCounterpart) {
432		this.trueCounterpart = trueCounterpart;
433	}
434
435	public Jid getTrueCounterpart() {
436		return this.trueCounterpart;
437	}
438
439	public Transferable getTransferable() {
440		return this.transferable;
441	}
442
443	public synchronized void setTransferable(Transferable transferable) {
444		this.fileParams = null;
445		this.transferable = transferable;
446	}
447
448	public boolean addReadByMarker(ReadByMarker readByMarker) {
449		if (readByMarker.getRealJid() != null) {
450			if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
451				return false;
452			}
453		} else if (readByMarker.getFullJid() != null) {
454			if (readByMarker.getFullJid().equals(counterpart)) {
455				return false;
456			}
457		}
458		if (this.readByMarkers.add(readByMarker)) {
459			if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
460				Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
461				while (iterator.hasNext()) {
462					ReadByMarker marker = iterator.next();
463					if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
464						iterator.remove();
465					}
466				}
467			}
468			return true;
469		} else {
470			return false;
471		}
472	}
473
474	public Set<ReadByMarker> getReadByMarkers() {
475		return Collections.unmodifiableSet(this.readByMarkers);
476	}
477
478	public boolean similar(Message message) {
479		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
480			return this.serverMsgId.equals(message.getServerMsgId());
481		} else if (this.body == null || this.counterpart == null) {
482			return false;
483		} else {
484			String body, otherBody;
485			if (this.hasFileOnRemoteHost()) {
486				body = getFileParams().url.toString();
487				otherBody = message.body == null ? null : message.body.trim();
488			} else {
489				body = this.body;
490				otherBody = message.body;
491			}
492			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
493			if (message.getRemoteMsgId() != null) {
494				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
495				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
496					return true;
497				}
498				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
499						&& matchingCounterpart
500						&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
501			} else {
502				return this.remoteMsgId == null
503						&& matchingCounterpart
504						&& body.equals(otherBody)
505						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
506			}
507		}
508	}
509
510	public Message next() {
511		if (this.conversation instanceof Conversation) {
512			final Conversation conversation = (Conversation) this.conversation;
513			synchronized (conversation.messages) {
514				if (this.mNextMessage == null) {
515					int index = conversation.messages.indexOf(this);
516					if (index < 0 || index >= conversation.messages.size() - 1) {
517						this.mNextMessage = null;
518					} else {
519						this.mNextMessage = conversation.messages.get(index + 1);
520					}
521				}
522				return this.mNextMessage;
523			}
524		} else {
525			throw new AssertionError("Calling next should be disabled for stubs");
526		}
527	}
528
529	public Message prev() {
530		if (this.conversation instanceof Conversation) {
531			final Conversation conversation = (Conversation) this.conversation;
532			synchronized (conversation.messages) {
533				if (this.mPreviousMessage == null) {
534					int index = conversation.messages.indexOf(this);
535					if (index <= 0 || index > conversation.messages.size()) {
536						this.mPreviousMessage = null;
537					} else {
538						this.mPreviousMessage = conversation.messages.get(index - 1);
539					}
540				}
541			}
542			return this.mPreviousMessage;
543		} else {
544			throw new AssertionError("Calling prev should be disabled for stubs");
545		}
546	}
547
548	public boolean isLastCorrectableMessage() {
549		Message next = next();
550		while (next != null) {
551			if (next.isCorrectable()) {
552				return false;
553			}
554			next = next.next();
555		}
556		return isCorrectable();
557	}
558
559	private boolean isCorrectable() {
560		return getStatus() != STATUS_RECEIVED && !isCarbon();
561	}
562
563	public boolean mergeable(final Message message) {
564		return message != null &&
565				(message.getType() == Message.TYPE_TEXT &&
566						this.getTransferable() == null &&
567						message.getTransferable() == null &&
568						message.getEncryption() != Message.ENCRYPTION_PGP &&
569						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
570						this.getType() == message.getType() &&
571						//this.getStatus() == message.getStatus() &&
572						isStatusMergeable(this.getStatus(), message.getStatus()) &&
573						this.getEncryption() == message.getEncryption() &&
574						this.getCounterpart() != null &&
575						this.getCounterpart().equals(message.getCounterpart()) &&
576						this.edited() == message.edited() &&
577						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
578						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
579						!message.isGeoUri() &&
580						!this.isGeoUri() &&
581						!message.treatAsDownloadable() &&
582						!this.treatAsDownloadable() &&
583						!message.getBody().startsWith(ME_COMMAND) &&
584						!this.getBody().startsWith(ME_COMMAND) &&
585						!this.bodyIsOnlyEmojis() &&
586						!message.bodyIsOnlyEmojis() &&
587						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
588						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
589						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
590						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
591				);
592	}
593
594	private static boolean isStatusMergeable(int a, int b) {
595		return a == b || (
596				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
597						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
598						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
599						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
600						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
601		);
602	}
603
604	public void setCounterparts(List<MucOptions.User> counterparts) {
605		this.counterparts = counterparts;
606	}
607
608	public List<MucOptions.User> getCounterparts() {
609		return this.counterparts;
610	}
611
612	public static class MergeSeparator {
613	}
614
615	public SpannableStringBuilder getMergedBody() {
616		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
617		Message current = this;
618		while (current.mergeable(current.next())) {
619			current = current.next();
620			if (current == null) {
621				break;
622			}
623			body.append("\n\n");
624			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
625					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
626			body.append(current.getBody().trim());
627		}
628		return body;
629	}
630
631	public boolean hasMeCommand() {
632		return this.body.trim().startsWith(ME_COMMAND);
633	}
634
635	public int getMergedStatus() {
636		int status = this.status;
637		Message current = this;
638		while (current.mergeable(current.next())) {
639			current = current.next();
640			if (current == null) {
641				break;
642			}
643			status = current.status;
644		}
645		return status;
646	}
647
648	public long getMergedTimeSent() {
649		long time = this.timeSent;
650		Message current = this;
651		while (current.mergeable(current.next())) {
652			current = current.next();
653			if (current == null) {
654				break;
655			}
656			time = current.timeSent;
657		}
658		return time;
659	}
660
661	public boolean wasMergedIntoPrevious() {
662		Message prev = this.prev();
663		return prev != null && prev.mergeable(this);
664	}
665
666	public boolean trusted() {
667		Contact contact = this.getContact();
668		return status > STATUS_RECEIVED || (contact != null && (contact.showInRoster() || contact.isSelf()));
669	}
670
671	public boolean fixCounterpart() {
672		Presences presences = conversation.getContact().getPresences();
673		if (counterpart != null && presences.has(counterpart.getResource())) {
674			return true;
675		} else if (presences.size() >= 1) {
676			try {
677				counterpart = Jid.of(conversation.getJid().getLocal(),
678						conversation.getJid().getDomain(),
679						presences.toResourceArray()[0]);
680				return true;
681			} catch (IllegalArgumentException e) {
682				counterpart = null;
683				return false;
684			}
685		} else {
686			counterpart = null;
687			return false;
688		}
689	}
690
691	public void setUuid(String uuid) {
692		this.uuid = uuid;
693	}
694
695	public String getEditedId() {
696		return edited;
697	}
698
699	public void setOob(boolean isOob) {
700		this.oob = isOob;
701	}
702
703	public String getMimeType() {
704		String extension;
705		if (relativeFilePath != null) {
706			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
707		} else {
708			try {
709				final URL url = new URL(body.split("\n")[0]);
710				extension = MimeUtils.extractRelevantExtension(url);
711			} catch (MalformedURLException e) {
712				return null;
713			}
714		}
715		return MimeUtils.guessMimeTypeFromExtension(extension);
716	}
717
718	public synchronized boolean treatAsDownloadable() {
719		if (treatAsDownloadable == null) {
720			try {
721				final String[] lines = body.split("\n");
722				if (lines.length == 0) {
723					treatAsDownloadable = false;
724					return false;
725				}
726				for (String line : lines) {
727					if (line.contains("\\s+")) {
728						treatAsDownloadable = false;
729						return false;
730					}
731				}
732				final URL url = new URL(lines[0]);
733				final String ref = url.getRef();
734				final String protocol = url.getProtocol();
735				final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
736				final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
737				final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
738				final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
739				treatAsDownloadable = validAesGcm || validOob;
740			} catch (MalformedURLException e) {
741				treatAsDownloadable = false;
742			}
743		}
744		return treatAsDownloadable;
745	}
746
747	public synchronized boolean bodyIsOnlyEmojis() {
748		if (isEmojisOnly == null) {
749			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
750		}
751		return isEmojisOnly;
752	}
753
754	public synchronized boolean isGeoUri() {
755		if (isGeoUri == null) {
756			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
757		}
758		return isGeoUri;
759	}
760
761	public synchronized void resetFileParams() {
762		this.fileParams = null;
763	}
764
765	public synchronized FileParams getFileParams() {
766		if (fileParams == null) {
767			fileParams = new FileParams();
768			if (this.transferable != null) {
769				fileParams.size = this.transferable.getFileSize();
770			}
771			String parts[] = body == null ? new String[0] : body.split("\\|");
772			switch (parts.length) {
773				case 1:
774					try {
775						fileParams.size = Long.parseLong(parts[0]);
776					} catch (NumberFormatException e) {
777						fileParams.url = parseUrl(parts[0]);
778					}
779					break;
780				case 5:
781					fileParams.runtime = parseInt(parts[4]);
782				case 4:
783					fileParams.width = parseInt(parts[2]);
784					fileParams.height = parseInt(parts[3]);
785				case 2:
786					fileParams.url = parseUrl(parts[0]);
787					fileParams.size = parseLong(parts[1]);
788					break;
789				case 3:
790					fileParams.size = parseLong(parts[0]);
791					fileParams.width = parseInt(parts[1]);
792					fileParams.height = parseInt(parts[2]);
793					break;
794			}
795		}
796		return fileParams;
797	}
798
799	private static long parseLong(String value) {
800		try {
801			return Long.parseLong(value);
802		} catch (NumberFormatException e) {
803			return 0;
804		}
805	}
806
807	private static int parseInt(String value) {
808		try {
809			return Integer.parseInt(value);
810		} catch (NumberFormatException e) {
811			return 0;
812		}
813	}
814
815	private static URL parseUrl(String value) {
816		try {
817			return new URL(value);
818		} catch (MalformedURLException e) {
819			return null;
820		}
821	}
822
823	public void untie() {
824		this.mNextMessage = null;
825		this.mPreviousMessage = null;
826	}
827
828	public boolean isFileOrImage() {
829		return type == TYPE_FILE || type == TYPE_IMAGE;
830	}
831
832	public boolean hasFileOnRemoteHost() {
833		return isFileOrImage() && getFileParams().url != null;
834	}
835
836	public boolean needsUploading() {
837		return isFileOrImage() && getFileParams().url == null;
838	}
839
840	public class FileParams {
841		public URL url;
842		public long size = 0;
843		public int width = 0;
844		public int height = 0;
845		public int runtime = 0;
846	}
847
848	public void setFingerprint(String fingerprint) {
849		this.axolotlFingerprint = fingerprint;
850	}
851
852	public String getFingerprint() {
853		return axolotlFingerprint;
854	}
855
856	public boolean isTrusted() {
857		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
858		return s != null && s.isTrusted();
859	}
860
861	private int getPreviousEncryption() {
862		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
863			if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
864				continue;
865			}
866			return iterator.getEncryption();
867		}
868		return ENCRYPTION_NONE;
869	}
870
871	private int getNextEncryption() {
872		if (this.conversation instanceof Conversation) {
873			Conversation conversation = (Conversation) this.conversation;
874			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
875				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
876					continue;
877				}
878				return iterator.getEncryption();
879			}
880			return conversation.getNextEncryption();
881		} else {
882			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
883		}
884	}
885
886	public boolean isValidInSession() {
887		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
888		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
889
890		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
891				|| futureEncryption == ENCRYPTION_NONE
892				|| pastEncryption != futureEncryption;
893
894		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
895	}
896
897	private static int getCleanedEncryption(int encryption) {
898		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
899			return ENCRYPTION_PGP;
900		}
901		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
902			return ENCRYPTION_AXOLOTL;
903		}
904		return encryption;
905	}
906}