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