Message.java

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