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