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