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 DELETED = "deleted";
 78	public static final String ME_COMMAND = "/me ";
 79
 80	public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
 81
 82
 83	public boolean markable = false;
 84	protected String conversationUuid;
 85	protected Jid counterpart;
 86	protected Jid trueCounterpart;
 87	protected String body;
 88	protected String encryptedBody;
 89	protected long timeSent;
 90	protected int encryption;
 91	protected int status;
 92	protected int type;
 93	protected boolean deleted = false;
 94	protected boolean carbon = false;
 95	protected boolean oob = false;
 96	protected List<Edited> edits = new ArrayList<>();
 97	protected String relativeFilePath;
 98	protected boolean read = true;
 99	protected String remoteMsgId = null;
100	protected String serverMsgId = null;
101	private final Conversational conversation;
102	protected Transferable transferable = null;
103	private Message mNextMessage = null;
104	private Message mPreviousMessage = null;
105	private String axolotlFingerprint = null;
106	private String errorMessage = null;
107	private Set<ReadByMarker> readByMarkers = new HashSet<>();
108
109	private Boolean isGeoUri = null;
110	private Boolean isEmojisOnly = null;
111	private Boolean treatAsDownloadable = null;
112	private FileParams fileParams = null;
113	private List<MucOptions.User> counterparts;
114	private WeakReference<MucOptions.User> user;
115
116	protected Message(Conversational conversation) {
117		this.conversation = conversation;
118	}
119
120	public Message(Conversational conversation, String body, int encryption) {
121		this(conversation, body, encryption, STATUS_UNSEND);
122	}
123
124	public Message(Conversational conversation, String body, int encryption, int status) {
125		this(conversation, java.util.UUID.randomUUID().toString(),
126				conversation.getUuid(),
127				conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
128				null,
129				body,
130				System.currentTimeMillis(),
131				encryption,
132				status,
133				TYPE_TEXT,
134				false,
135				null,
136				null,
137				null,
138				null,
139				true,
140				null,
141				false,
142				null,
143				null,
144				false,
145				false);
146	}
147
148	protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
149	                final Jid trueCounterpart, final String body, final long timeSent,
150	                final int encryption, final int status, final int type, final boolean carbon,
151	                final String remoteMsgId, final String relativeFilePath,
152	                final String serverMsgId, final String fingerprint, final boolean read,
153	                final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
154	                final boolean markable, final boolean deleted) {
155		this.conversation = conversation;
156		this.uuid = uuid;
157		this.conversationUuid = conversationUUid;
158		this.counterpart = counterpart;
159		this.trueCounterpart = trueCounterpart;
160		this.body = body == null ? "" : body;
161		this.timeSent = timeSent;
162		this.encryption = encryption;
163		this.status = status;
164		this.type = type;
165		this.carbon = carbon;
166		this.remoteMsgId = remoteMsgId;
167		this.relativeFilePath = relativeFilePath;
168		this.serverMsgId = serverMsgId;
169		this.axolotlFingerprint = fingerprint;
170		this.read = read;
171		this.edits = Edited.fromJson(edited);
172		this.oob = oob;
173		this.errorMessage = errorMessage;
174		this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
175		this.markable = markable;
176		this.deleted = deleted;
177	}
178
179	public static Message fromCursor(Cursor cursor, Conversation conversation) {
180		Jid jid;
181		try {
182			String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
183			if (value != null) {
184				jid = Jid.of(value);
185			} else {
186				jid = null;
187			}
188		} catch (IllegalArgumentException e) {
189			jid = null;
190		} catch (IllegalStateException e) {
191			return null; // message too long?
192		}
193		Jid trueCounterpart;
194		try {
195			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
196			if (value != null) {
197				trueCounterpart = Jid.of(value);
198			} else {
199				trueCounterpart = null;
200			}
201		} catch (IllegalArgumentException e) {
202			trueCounterpart = null;
203		}
204		return new Message(conversation,
205				cursor.getString(cursor.getColumnIndex(UUID)),
206				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
207				jid,
208				trueCounterpart,
209				cursor.getString(cursor.getColumnIndex(BODY)),
210				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
211				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
212				cursor.getInt(cursor.getColumnIndex(STATUS)),
213				cursor.getInt(cursor.getColumnIndex(TYPE)),
214				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
215				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
216				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
217				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
218				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
219				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
220				cursor.getString(cursor.getColumnIndex(EDITED)),
221				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
222				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
223				ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
224				cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
225				cursor.getInt(cursor.getColumnIndex(DELETED)) > 0);
226	}
227
228	public static Message createStatusMessage(Conversation conversation, String body) {
229		final Message message = new Message(conversation);
230		message.setType(Message.TYPE_STATUS);
231		message.setStatus(Message.STATUS_RECEIVED);
232		message.body = body;
233		return message;
234	}
235
236	public static Message createLoadMoreMessage(Conversation conversation) {
237		final Message message = new Message(conversation);
238		message.setType(Message.TYPE_STATUS);
239		message.body = "LOAD_MORE";
240		return message;
241	}
242
243	@Override
244	public ContentValues getContentValues() {
245		ContentValues values = new ContentValues();
246		values.put(UUID, uuid);
247		values.put(CONVERSATION, conversationUuid);
248		if (counterpart == null) {
249			values.putNull(COUNTERPART);
250		} else {
251			values.put(COUNTERPART, counterpart.toString());
252		}
253		if (trueCounterpart == null) {
254			values.putNull(TRUE_COUNTERPART);
255		} else {
256			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
257		}
258		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
259		values.put(TIME_SENT, timeSent);
260		values.put(ENCRYPTION, encryption);
261		values.put(STATUS, status);
262		values.put(TYPE, type);
263		values.put(CARBON, carbon ? 1 : 0);
264		values.put(REMOTE_MSG_ID, remoteMsgId);
265		values.put(RELATIVE_FILE_PATH, relativeFilePath);
266		values.put(SERVER_MSG_ID, serverMsgId);
267		values.put(FINGERPRINT, axolotlFingerprint);
268		values.put(READ, read ? 1 : 0);
269		try {
270			values.put(EDITED, Edited.toJson(edits));
271		} catch (JSONException e) {
272			Log.e(Config.LOGTAG,"error persisting json for edits",e);
273		}
274		values.put(OOB, oob ? 1 : 0);
275		values.put(ERROR_MESSAGE, errorMessage);
276		values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
277		values.put(MARKABLE, markable ? 1 : 0);
278		values.put(DELETED, deleted ? 1 : 0);
279		return values;
280	}
281
282	public String getConversationUuid() {
283		return conversationUuid;
284	}
285
286	public Conversational getConversation() {
287		return this.conversation;
288	}
289
290	public Jid getCounterpart() {
291		return counterpart;
292	}
293
294	public void setCounterpart(final Jid counterpart) {
295		this.counterpart = counterpart;
296	}
297
298	public Contact getContact() {
299		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
300			return this.conversation.getContact();
301		} else {
302			if (this.trueCounterpart == null) {
303				return null;
304			} else {
305				return this.conversation.getAccount().getRoster()
306						.getContactFromContactList(this.trueCounterpart);
307			}
308		}
309	}
310
311	public String getBody() {
312		return body;
313	}
314
315	public synchronized void setBody(String body) {
316		if (body == null) {
317			throw new Error("You should not set the message body to null");
318		}
319		this.body = body;
320		this.isGeoUri = null;
321		this.isEmojisOnly = null;
322		this.treatAsDownloadable = null;
323		this.fileParams = null;
324	}
325
326	public void setMucUser(MucOptions.User user) {
327		this.user = new WeakReference<>(user);
328	}
329
330	public boolean sameMucUser(Message otherMessage) {
331		final MucOptions.User thisUser = this.user == null ? null : this.user.get();
332		final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
333		return thisUser != null && thisUser == otherUser;
334	}
335
336	public String getErrorMessage() {
337		return errorMessage;
338	}
339
340	public boolean setErrorMessage(String message) {
341		boolean changed = (message != null && !message.equals(errorMessage))
342				|| (message == null && errorMessage != null);
343		this.errorMessage = message;
344		return changed;
345	}
346
347	public long getTimeSent() {
348		return timeSent;
349	}
350
351	public int getEncryption() {
352		return encryption;
353	}
354
355	public void setEncryption(int encryption) {
356		this.encryption = encryption;
357	}
358
359	public int getStatus() {
360		return status;
361	}
362
363	public void setStatus(int status) {
364		this.status = status;
365	}
366
367	public String getRelativeFilePath() {
368		return this.relativeFilePath;
369	}
370
371	public void setRelativeFilePath(String path) {
372		this.relativeFilePath = path;
373	}
374
375	public String getRemoteMsgId() {
376		return this.remoteMsgId;
377	}
378
379	public void setRemoteMsgId(String id) {
380		this.remoteMsgId = id;
381	}
382
383	public String getServerMsgId() {
384		return this.serverMsgId;
385	}
386
387	public void setServerMsgId(String id) {
388		this.serverMsgId = id;
389	}
390
391	public boolean isRead() {
392		return this.read;
393	}
394
395	public boolean isDeleted() {
396		return this.deleted;
397	}
398
399	public void setDeleted(boolean deleted) {
400		this.deleted = deleted;
401	}
402
403	public void markRead() {
404		this.read = true;
405	}
406
407	public void markUnread() {
408		this.read = false;
409	}
410
411	public void setTime(long time) {
412		this.timeSent = time;
413	}
414
415	public String getEncryptedBody() {
416		return this.encryptedBody;
417	}
418
419	public void setEncryptedBody(String body) {
420		this.encryptedBody = body;
421	}
422
423	public int getType() {
424		return this.type;
425	}
426
427	public void setType(int type) {
428		this.type = type;
429	}
430
431	public boolean isCarbon() {
432		return carbon;
433	}
434
435	public void setCarbon(boolean carbon) {
436		this.carbon = carbon;
437	}
438
439	public void putEdited(String edited, String serverMsgId) {
440		this.edits.add(new Edited(edited, serverMsgId));
441	}
442
443	public boolean edited() {
444		return this.edits.size() > 0;
445	}
446
447	public void setTrueCounterpart(Jid trueCounterpart) {
448		this.trueCounterpart = trueCounterpart;
449	}
450
451	public Jid getTrueCounterpart() {
452		return this.trueCounterpart;
453	}
454
455	public Transferable getTransferable() {
456		return this.transferable;
457	}
458
459	public synchronized void setTransferable(Transferable transferable) {
460		this.fileParams = null;
461		this.transferable = transferable;
462	}
463
464	public boolean addReadByMarker(ReadByMarker readByMarker) {
465		if (readByMarker.getRealJid() != null) {
466			if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
467				return false;
468			}
469		} else if (readByMarker.getFullJid() != null) {
470			if (readByMarker.getFullJid().equals(counterpart)) {
471				return false;
472			}
473		}
474		if (this.readByMarkers.add(readByMarker)) {
475			if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
476				Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
477				while (iterator.hasNext()) {
478					ReadByMarker marker = iterator.next();
479					if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
480						iterator.remove();
481					}
482				}
483			}
484			return true;
485		} else {
486			return false;
487		}
488	}
489
490	public Set<ReadByMarker> getReadByMarkers() {
491		return Collections.unmodifiableSet(this.readByMarkers);
492	}
493
494	boolean similar(Message message) {
495		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
496			return this.serverMsgId.equals(message.getServerMsgId()) || Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
497		} else if (Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
498			return true;
499		} else if (this.body == null || this.counterpart == null) {
500			return false;
501		} else {
502			String body, otherBody;
503			if (this.hasFileOnRemoteHost()) {
504				body = getFileParams().url.toString();
505				otherBody = message.body == null ? null : message.body.trim();
506			} else {
507				body = this.body;
508				otherBody = message.body;
509			}
510			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
511			if (message.getRemoteMsgId() != null) {
512				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
513				if (hasUuid && matchingCounterpart && Edited.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
514					return true;
515				}
516				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
517						&& matchingCounterpart
518						&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
519			} else {
520				return this.remoteMsgId == null
521						&& matchingCounterpart
522						&& body.equals(otherBody)
523						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
524			}
525		}
526	}
527
528	public Message next() {
529		if (this.conversation instanceof Conversation) {
530			final Conversation conversation = (Conversation) this.conversation;
531			synchronized (conversation.messages) {
532				if (this.mNextMessage == null) {
533					int index = conversation.messages.indexOf(this);
534					if (index < 0 || index >= conversation.messages.size() - 1) {
535						this.mNextMessage = null;
536					} else {
537						this.mNextMessage = conversation.messages.get(index + 1);
538					}
539				}
540				return this.mNextMessage;
541			}
542		} else {
543			throw new AssertionError("Calling next should be disabled for stubs");
544		}
545	}
546
547	public Message prev() {
548		if (this.conversation instanceof Conversation) {
549			final Conversation conversation = (Conversation) this.conversation;
550			synchronized (conversation.messages) {
551				if (this.mPreviousMessage == null) {
552					int index = conversation.messages.indexOf(this);
553					if (index <= 0 || index > conversation.messages.size()) {
554						this.mPreviousMessage = null;
555					} else {
556						this.mPreviousMessage = conversation.messages.get(index - 1);
557					}
558				}
559			}
560			return this.mPreviousMessage;
561		} else {
562			throw new AssertionError("Calling prev should be disabled for stubs");
563		}
564	}
565
566	public boolean isLastCorrectableMessage() {
567		Message next = next();
568		while (next != null) {
569			if (next.isCorrectable()) {
570				return false;
571			}
572			next = next.next();
573		}
574		return isCorrectable();
575	}
576
577	private boolean isCorrectable() {
578		return getStatus() != STATUS_RECEIVED && !isCarbon();
579	}
580
581	public boolean mergeable(final Message message) {
582		return message != null &&
583				(message.getType() == Message.TYPE_TEXT &&
584						this.getTransferable() == null &&
585						message.getTransferable() == null &&
586						message.getEncryption() != Message.ENCRYPTION_PGP &&
587						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
588						this.getType() == message.getType() &&
589						//this.getStatus() == message.getStatus() &&
590						isStatusMergeable(this.getStatus(), message.getStatus()) &&
591						this.getEncryption() == message.getEncryption() &&
592						this.getCounterpart() != null &&
593						this.getCounterpart().equals(message.getCounterpart()) &&
594						this.edited() == message.edited() &&
595						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
596						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
597						!message.isGeoUri() &&
598						!this.isGeoUri() &&
599						!message.treatAsDownloadable() &&
600						!this.treatAsDownloadable() &&
601						!message.getBody().startsWith(ME_COMMAND) &&
602						!this.getBody().startsWith(ME_COMMAND) &&
603						!this.bodyIsOnlyEmojis() &&
604						!message.bodyIsOnlyEmojis() &&
605						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
606						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
607						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
608						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
609				);
610	}
611
612	private static boolean isStatusMergeable(int a, int b) {
613		return a == b || (
614				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
615						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
616						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
617						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
618						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
619		);
620	}
621
622	public void setCounterparts(List<MucOptions.User> counterparts) {
623		this.counterparts = counterparts;
624	}
625
626	public List<MucOptions.User> getCounterparts() {
627		return this.counterparts;
628	}
629
630	public static class MergeSeparator {
631	}
632
633	public SpannableStringBuilder getMergedBody() {
634		SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
635		Message current = this;
636		while (current.mergeable(current.next())) {
637			current = current.next();
638			if (current == null) {
639				break;
640			}
641			body.append("\n\n");
642			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
643					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
644			body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
645		}
646		return body;
647	}
648
649	public boolean hasMeCommand() {
650		return this.body.trim().startsWith(ME_COMMAND);
651	}
652
653	public int getMergedStatus() {
654		int status = this.status;
655		Message current = this;
656		while (current.mergeable(current.next())) {
657			current = current.next();
658			if (current == null) {
659				break;
660			}
661			status = current.status;
662		}
663		return status;
664	}
665
666	public long getMergedTimeSent() {
667		long time = this.timeSent;
668		Message current = this;
669		while (current.mergeable(current.next())) {
670			current = current.next();
671			if (current == null) {
672				break;
673			}
674			time = current.timeSent;
675		}
676		return time;
677	}
678
679	public boolean wasMergedIntoPrevious() {
680		Message prev = this.prev();
681		return prev != null && prev.mergeable(this);
682	}
683
684	public boolean trusted() {
685		Contact contact = this.getContact();
686		return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
687	}
688
689	public boolean fixCounterpart() {
690		Presences presences = conversation.getContact().getPresences();
691		if (counterpart != null && presences.has(counterpart.getResource())) {
692			return true;
693		} else if (presences.size() >= 1) {
694			try {
695				counterpart = Jid.of(conversation.getJid().getLocal(),
696						conversation.getJid().getDomain(),
697						presences.toResourceArray()[0]);
698				return true;
699			} catch (IllegalArgumentException e) {
700				counterpart = null;
701				return false;
702			}
703		} else {
704			counterpart = null;
705			return false;
706		}
707	}
708
709	public void setUuid(String uuid) {
710		this.uuid = uuid;
711	}
712
713	public String getEditedId() {
714		if (edits.size() > 0) {
715			return edits.get(edits.size() - 1).getEditedId();
716		} else {
717			throw new IllegalStateException("Attempting to store unedited message");
718		}
719	}
720
721	public void setOob(boolean isOob) {
722		this.oob = isOob;
723	}
724
725	public String getMimeType() {
726		String extension;
727		if (relativeFilePath != null) {
728			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
729		} else {
730			try {
731				final URL url = new URL(body.split("\n")[0]);
732				extension = MimeUtils.extractRelevantExtension(url);
733			} catch (MalformedURLException e) {
734				return null;
735			}
736		}
737		return MimeUtils.guessMimeTypeFromExtension(extension);
738	}
739
740	public synchronized boolean treatAsDownloadable() {
741		if (treatAsDownloadable == null) {
742			treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
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 || encryption == ENCRYPTION_AXOLOTL_FAILED) {
902			return ENCRYPTION_AXOLOTL;
903		}
904		return encryption;
905	}
906}