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<Edit> 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 = Edit.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, Edit.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		final Edit edit = new Edit(edited, serverMsgId);
438		if (this.edits.size() < 128 && !this.edits.contains(edit)) {
439			this.edits.add(edit);
440		}
441	}
442
443	boolean remoteMsgIdMatchInEdit(String id) {
444		for(Edit edit : this.edits) {
445			if (id.equals(edit.getEditedId())) {
446				return true;
447			}
448		}
449		return false;
450	}
451
452	public String getBodyLanguage() {
453		return this.bodyLanguage;
454	}
455
456	public void setBodyLanguage(String language) {
457		this.bodyLanguage = language;
458	}
459
460	public boolean edited() {
461		return this.edits.size() > 0;
462	}
463
464	public void setTrueCounterpart(Jid trueCounterpart) {
465		this.trueCounterpart = trueCounterpart;
466	}
467
468	public Jid getTrueCounterpart() {
469		return this.trueCounterpart;
470	}
471
472	public Transferable getTransferable() {
473		return this.transferable;
474	}
475
476	public synchronized void setTransferable(Transferable transferable) {
477		this.fileParams = null;
478		this.transferable = transferable;
479	}
480
481	public boolean addReadByMarker(ReadByMarker readByMarker) {
482		if (readByMarker.getRealJid() != null) {
483			if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
484				return false;
485			}
486		} else if (readByMarker.getFullJid() != null) {
487			if (readByMarker.getFullJid().equals(counterpart)) {
488				return false;
489			}
490		}
491		if (this.readByMarkers.add(readByMarker)) {
492			if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
493				Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
494				while (iterator.hasNext()) {
495					ReadByMarker marker = iterator.next();
496					if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
497						iterator.remove();
498					}
499				}
500			}
501			return true;
502		} else {
503			return false;
504		}
505	}
506
507	public Set<ReadByMarker> getReadByMarkers() {
508		return Collections.unmodifiableSet(this.readByMarkers);
509	}
510
511	boolean similar(Message message) {
512		if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
513			return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
514		} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
515			return true;
516		} else if (this.body == null || this.counterpart == null) {
517			return false;
518		} else {
519			String body, otherBody;
520			if (this.hasFileOnRemoteHost()) {
521				body = getFileParams().url.toString();
522				otherBody = message.body == null ? null : message.body.trim();
523			} else {
524				body = this.body;
525				otherBody = message.body;
526			}
527			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
528			if (message.getRemoteMsgId() != null) {
529				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
530				if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
531					return true;
532				}
533				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
534						&& matchingCounterpart
535						&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
536			} else {
537				return this.remoteMsgId == null
538						&& matchingCounterpart
539						&& body.equals(otherBody)
540						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
541			}
542		}
543	}
544
545	public Message next() {
546		if (this.conversation instanceof Conversation) {
547			final Conversation conversation = (Conversation) this.conversation;
548			synchronized (conversation.messages) {
549				if (this.mNextMessage == null) {
550					int index = conversation.messages.indexOf(this);
551					if (index < 0 || index >= conversation.messages.size() - 1) {
552						this.mNextMessage = null;
553					} else {
554						this.mNextMessage = conversation.messages.get(index + 1);
555					}
556				}
557				return this.mNextMessage;
558			}
559		} else {
560			throw new AssertionError("Calling next should be disabled for stubs");
561		}
562	}
563
564	public Message prev() {
565		if (this.conversation instanceof Conversation) {
566			final Conversation conversation = (Conversation) this.conversation;
567			synchronized (conversation.messages) {
568				if (this.mPreviousMessage == null) {
569					int index = conversation.messages.indexOf(this);
570					if (index <= 0 || index > conversation.messages.size()) {
571						this.mPreviousMessage = null;
572					} else {
573						this.mPreviousMessage = conversation.messages.get(index - 1);
574					}
575				}
576			}
577			return this.mPreviousMessage;
578		} else {
579			throw new AssertionError("Calling prev should be disabled for stubs");
580		}
581	}
582
583	public boolean isLastCorrectableMessage() {
584		Message next = next();
585		while (next != null) {
586			if (next.isCorrectable()) {
587				return false;
588			}
589			next = next.next();
590		}
591		return isCorrectable();
592	}
593
594	private boolean isCorrectable() {
595		return getStatus() != STATUS_RECEIVED && !isCarbon();
596	}
597
598	public boolean mergeable(final Message message) {
599		return message != null &&
600				(message.getType() == Message.TYPE_TEXT &&
601						this.getTransferable() == null &&
602						message.getTransferable() == null &&
603						message.getEncryption() != Message.ENCRYPTION_PGP &&
604						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
605						this.getType() == message.getType() &&
606						//this.getStatus() == message.getStatus() &&
607						isStatusMergeable(this.getStatus(), message.getStatus()) &&
608						this.getEncryption() == message.getEncryption() &&
609						this.getCounterpart() != null &&
610						this.getCounterpart().equals(message.getCounterpart()) &&
611						this.edited() == message.edited() &&
612						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
613						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
614						!message.isGeoUri() &&
615						!this.isGeoUri() &&
616						!message.isOOb() &&
617						!this.isOOb() &&
618						!message.treatAsDownloadable() &&
619						!this.treatAsDownloadable() &&
620						!message.getBody().startsWith(ME_COMMAND) &&
621						!this.getBody().startsWith(ME_COMMAND) &&
622						!this.bodyIsOnlyEmojis() &&
623						!message.bodyIsOnlyEmojis() &&
624						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
625						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
626						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
627						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
628				);
629	}
630
631	private static boolean isStatusMergeable(int a, int b) {
632		return a == b || (
633				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
634						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
635						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
636						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
637						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
638		);
639	}
640
641	public void setCounterparts(List<MucOptions.User> counterparts) {
642		this.counterparts = counterparts;
643	}
644
645	public List<MucOptions.User> getCounterparts() {
646		return this.counterparts;
647	}
648
649	@Override
650	public int getAvatarBackgroundColor() {
651		if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
652			return Color.TRANSPARENT;
653		} else {
654			return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
655		}
656	}
657
658	public boolean isOOb() {
659		return oob;
660	}
661
662	public static class MergeSeparator {
663	}
664
665	public SpannableStringBuilder getMergedBody() {
666		SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
667		Message current = this;
668		while (current.mergeable(current.next())) {
669			current = current.next();
670			if (current == null) {
671				break;
672			}
673			body.append("\n\n");
674			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
675					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
676			body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
677		}
678		return body;
679	}
680
681	public boolean hasMeCommand() {
682		return this.body.trim().startsWith(ME_COMMAND);
683	}
684
685	public int getMergedStatus() {
686		int status = this.status;
687		Message current = this;
688		while (current.mergeable(current.next())) {
689			current = current.next();
690			if (current == null) {
691				break;
692			}
693			status = current.status;
694		}
695		return status;
696	}
697
698	public long getMergedTimeSent() {
699		long time = this.timeSent;
700		Message current = this;
701		while (current.mergeable(current.next())) {
702			current = current.next();
703			if (current == null) {
704				break;
705			}
706			time = current.timeSent;
707		}
708		return time;
709	}
710
711	public boolean wasMergedIntoPrevious() {
712		Message prev = this.prev();
713		return prev != null && prev.mergeable(this);
714	}
715
716	public boolean trusted() {
717		Contact contact = this.getContact();
718		return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
719	}
720
721	public boolean fixCounterpart() {
722		Presences presences = conversation.getContact().getPresences();
723		if (counterpart != null && presences.has(counterpart.getResource())) {
724			return true;
725		} else if (presences.size() >= 1) {
726			try {
727				counterpart = Jid.of(conversation.getJid().getLocal(),
728						conversation.getJid().getDomain(),
729						presences.toResourceArray()[0]);
730				return true;
731			} catch (IllegalArgumentException e) {
732				counterpart = null;
733				return false;
734			}
735		} else {
736			counterpart = null;
737			return false;
738		}
739	}
740
741	public void setUuid(String uuid) {
742		this.uuid = uuid;
743	}
744
745	public String getEditedId() {
746		if (edits.size() > 0) {
747			return edits.get(edits.size() - 1).getEditedId();
748		} else {
749			throw new IllegalStateException("Attempting to store unedited message");
750		}
751	}
752
753	public String getEditedIdWireFormat() {
754		if (edits.size() > 0) {
755			return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
756		} else {
757			throw new IllegalStateException("Attempting to store unedited message");
758		}
759	}
760
761	public void setOob(boolean isOob) {
762		this.oob = isOob;
763	}
764
765	public String getMimeType() {
766		String extension;
767		if (relativeFilePath != null) {
768			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
769		} else {
770			try {
771				final URL url = new URL(body.split("\n")[0]);
772				extension = MimeUtils.extractRelevantExtension(url);
773			} catch (MalformedURLException e) {
774				return null;
775			}
776		}
777		return MimeUtils.guessMimeTypeFromExtension(extension);
778	}
779
780	public synchronized boolean treatAsDownloadable() {
781		if (treatAsDownloadable == null) {
782			treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
783		}
784		return treatAsDownloadable;
785	}
786
787	public synchronized boolean bodyIsOnlyEmojis() {
788		if (isEmojisOnly == null) {
789			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
790		}
791		return isEmojisOnly;
792	}
793
794	public synchronized boolean isGeoUri() {
795		if (isGeoUri == null) {
796			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
797		}
798		return isGeoUri;
799	}
800
801	public synchronized void resetFileParams() {
802		this.fileParams = null;
803	}
804
805	public synchronized FileParams getFileParams() {
806		if (fileParams == null) {
807			fileParams = new FileParams();
808			if (this.transferable != null) {
809				fileParams.size = this.transferable.getFileSize();
810			}
811			final String[] parts = body == null ? new String[0] : body.split("\\|");
812			switch (parts.length) {
813				case 1:
814					try {
815						fileParams.size = Long.parseLong(parts[0]);
816					} catch (NumberFormatException e) {
817						fileParams.url = parseUrl(parts[0]);
818					}
819					break;
820				case 5:
821					fileParams.runtime = parseInt(parts[4]);
822				case 4:
823					fileParams.width = parseInt(parts[2]);
824					fileParams.height = parseInt(parts[3]);
825				case 2:
826					fileParams.url = parseUrl(parts[0]);
827					fileParams.size = parseLong(parts[1]);
828					break;
829				case 3:
830					fileParams.size = parseLong(parts[0]);
831					fileParams.width = parseInt(parts[1]);
832					fileParams.height = parseInt(parts[2]);
833					break;
834			}
835		}
836		return fileParams;
837	}
838
839	private static long parseLong(String value) {
840		try {
841			return Long.parseLong(value);
842		} catch (NumberFormatException e) {
843			return 0;
844		}
845	}
846
847	private static int parseInt(String value) {
848		try {
849			return Integer.parseInt(value);
850		} catch (NumberFormatException e) {
851			return 0;
852		}
853	}
854
855	private static URL parseUrl(String value) {
856		try {
857			return new URL(value);
858		} catch (MalformedURLException e) {
859			return null;
860		}
861	}
862
863	public void untie() {
864		this.mNextMessage = null;
865		this.mPreviousMessage = null;
866	}
867
868	public boolean isPrivateMessage() {
869		return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
870	}
871
872	public boolean isFileOrImage() {
873		return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
874	}
875
876	public boolean hasFileOnRemoteHost() {
877		return isFileOrImage() && getFileParams().url != null;
878	}
879
880	public boolean needsUploading() {
881		return isFileOrImage() && getFileParams().url == null;
882	}
883
884	public class FileParams {
885		public URL url;
886		public long size = 0;
887		public int width = 0;
888		public int height = 0;
889		public int runtime = 0;
890	}
891
892	public void setFingerprint(String fingerprint) {
893		this.axolotlFingerprint = fingerprint;
894	}
895
896	public String getFingerprint() {
897		return axolotlFingerprint;
898	}
899
900	public boolean isTrusted() {
901		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
902		return s != null && s.isTrusted();
903	}
904
905	private int getPreviousEncryption() {
906		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
907			if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
908				continue;
909			}
910			return iterator.getEncryption();
911		}
912		return ENCRYPTION_NONE;
913	}
914
915	private int getNextEncryption() {
916		if (this.conversation instanceof Conversation) {
917			Conversation conversation = (Conversation) this.conversation;
918			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
919				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
920					continue;
921				}
922				return iterator.getEncryption();
923			}
924			return conversation.getNextEncryption();
925		} else {
926			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
927		}
928	}
929
930	public boolean isValidInSession() {
931		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
932		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
933
934		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
935				|| futureEncryption == ENCRYPTION_NONE
936				|| pastEncryption != futureEncryption;
937
938		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
939	}
940
941	private static int getCleanedEncryption(int encryption) {
942		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
943			return ENCRYPTION_PGP;
944		}
945		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
946			return ENCRYPTION_AXOLOTL;
947		}
948		return encryption;
949	}
950
951	public static boolean configurePrivateMessage(final Message message) {
952		return configurePrivateMessage(message, false);
953	}
954
955	public static boolean configurePrivateFileMessage(final Message message) {
956		return configurePrivateMessage(message, true);
957	}
958
959	private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
960		final Conversation conversation;
961		if (message.conversation instanceof Conversation) {
962			conversation = (Conversation) message.conversation;
963		} else {
964			return false;
965		}
966		if (conversation.getMode() == Conversation.MODE_MULTI) {
967			final Jid nextCounterpart = conversation.getNextCounterpart();
968			if (nextCounterpart != null) {
969				message.setCounterpart(nextCounterpart);
970				message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
971				message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
972				return true;
973			}
974		}
975		return false;
976	}
977}