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