Message.java

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