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.treatAsDownloadable() &&
617						!this.treatAsDownloadable() &&
618						!message.getBody().startsWith(ME_COMMAND) &&
619						!this.getBody().startsWith(ME_COMMAND) &&
620						!this.bodyIsOnlyEmojis() &&
621						!message.bodyIsOnlyEmojis() &&
622						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
623						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
624						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
625						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
626				);
627	}
628
629	private static boolean isStatusMergeable(int a, int b) {
630		return a == b || (
631				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
632						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
633						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
634						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
635						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
636		);
637	}
638
639	public void setCounterparts(List<MucOptions.User> counterparts) {
640		this.counterparts = counterparts;
641	}
642
643	public List<MucOptions.User> getCounterparts() {
644		return this.counterparts;
645	}
646
647	@Override
648	public int getAvatarBackgroundColor() {
649		if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
650			return Color.TRANSPARENT;
651		} else {
652			return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
653		}
654	}
655
656	public boolean isOOb() {
657		return oob;
658	}
659
660	public static class MergeSeparator {
661	}
662
663	public SpannableStringBuilder getMergedBody() {
664		SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
665		Message current = this;
666		while (current.mergeable(current.next())) {
667			current = current.next();
668			if (current == null) {
669				break;
670			}
671			body.append("\n\n");
672			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
673					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
674			body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
675		}
676		return body;
677	}
678
679	public boolean hasMeCommand() {
680		return this.body.trim().startsWith(ME_COMMAND);
681	}
682
683	public int getMergedStatus() {
684		int status = this.status;
685		Message current = this;
686		while (current.mergeable(current.next())) {
687			current = current.next();
688			if (current == null) {
689				break;
690			}
691			status = current.status;
692		}
693		return status;
694	}
695
696	public long getMergedTimeSent() {
697		long time = this.timeSent;
698		Message current = this;
699		while (current.mergeable(current.next())) {
700			current = current.next();
701			if (current == null) {
702				break;
703			}
704			time = current.timeSent;
705		}
706		return time;
707	}
708
709	public boolean wasMergedIntoPrevious() {
710		Message prev = this.prev();
711		return prev != null && prev.mergeable(this);
712	}
713
714	public boolean trusted() {
715		Contact contact = this.getContact();
716		return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
717	}
718
719	public boolean fixCounterpart() {
720		Presences presences = conversation.getContact().getPresences();
721		if (counterpart != null && presences.has(counterpart.getResource())) {
722			return true;
723		} else if (presences.size() >= 1) {
724			try {
725				counterpart = Jid.of(conversation.getJid().getLocal(),
726						conversation.getJid().getDomain(),
727						presences.toResourceArray()[0]);
728				return true;
729			} catch (IllegalArgumentException e) {
730				counterpart = null;
731				return false;
732			}
733		} else {
734			counterpart = null;
735			return false;
736		}
737	}
738
739	public void setUuid(String uuid) {
740		this.uuid = uuid;
741	}
742
743	public String getEditedId() {
744		if (edits.size() > 0) {
745			return edits.get(edits.size() - 1).getEditedId();
746		} else {
747			throw new IllegalStateException("Attempting to store unedited message");
748		}
749	}
750
751	public String getEditedIdWireFormat() {
752		if (edits.size() > 0) {
753			return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
754		} else {
755			throw new IllegalStateException("Attempting to store unedited message");
756		}
757	}
758
759	public void setOob(boolean isOob) {
760		this.oob = isOob;
761	}
762
763	public String getMimeType() {
764		String extension;
765		if (relativeFilePath != null) {
766			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
767		} else {
768			try {
769				final URL url = new URL(body.split("\n")[0]);
770				extension = MimeUtils.extractRelevantExtension(url);
771			} catch (MalformedURLException e) {
772				return null;
773			}
774		}
775		return MimeUtils.guessMimeTypeFromExtension(extension);
776	}
777
778	public synchronized boolean treatAsDownloadable() {
779		if (treatAsDownloadable == null) {
780			treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
781		}
782		return treatAsDownloadable;
783	}
784
785	public synchronized boolean bodyIsOnlyEmojis() {
786		if (isEmojisOnly == null) {
787			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
788		}
789		return isEmojisOnly;
790	}
791
792	public synchronized boolean isGeoUri() {
793		if (isGeoUri == null) {
794			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
795		}
796		return isGeoUri;
797	}
798
799	public synchronized void resetFileParams() {
800		this.fileParams = null;
801	}
802
803	public synchronized FileParams getFileParams() {
804		if (fileParams == null) {
805			fileParams = new FileParams();
806			if (this.transferable != null) {
807				fileParams.size = this.transferable.getFileSize();
808			}
809			final String[] parts = body == null ? new String[0] : body.split("\\|");
810			switch (parts.length) {
811				case 1:
812					try {
813						fileParams.size = Long.parseLong(parts[0]);
814					} catch (NumberFormatException e) {
815						fileParams.url = parseUrl(parts[0]);
816					}
817					break;
818				case 5:
819					fileParams.runtime = parseInt(parts[4]);
820				case 4:
821					fileParams.width = parseInt(parts[2]);
822					fileParams.height = parseInt(parts[3]);
823				case 2:
824					fileParams.url = parseUrl(parts[0]);
825					fileParams.size = parseLong(parts[1]);
826					break;
827				case 3:
828					fileParams.size = parseLong(parts[0]);
829					fileParams.width = parseInt(parts[1]);
830					fileParams.height = parseInt(parts[2]);
831					break;
832			}
833		}
834		return fileParams;
835	}
836
837	private static long parseLong(String value) {
838		try {
839			return Long.parseLong(value);
840		} catch (NumberFormatException e) {
841			return 0;
842		}
843	}
844
845	private static int parseInt(String value) {
846		try {
847			return Integer.parseInt(value);
848		} catch (NumberFormatException e) {
849			return 0;
850		}
851	}
852
853	private static URL parseUrl(String value) {
854		try {
855			return new URL(value);
856		} catch (MalformedURLException e) {
857			return null;
858		}
859	}
860
861	public void untie() {
862		this.mNextMessage = null;
863		this.mPreviousMessage = null;
864	}
865
866	public boolean isPrivateMessage() {
867		return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
868	}
869
870	public boolean isFileOrImage() {
871		return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
872	}
873
874	public boolean hasFileOnRemoteHost() {
875		return isFileOrImage() && getFileParams().url != null;
876	}
877
878	public boolean needsUploading() {
879		return isFileOrImage() && getFileParams().url == null;
880	}
881
882	public class FileParams {
883		public URL url;
884		public long size = 0;
885		public int width = 0;
886		public int height = 0;
887		public int runtime = 0;
888	}
889
890	public void setFingerprint(String fingerprint) {
891		this.axolotlFingerprint = fingerprint;
892	}
893
894	public String getFingerprint() {
895		return axolotlFingerprint;
896	}
897
898	public boolean isTrusted() {
899		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
900		return s != null && s.isTrusted();
901	}
902
903	private int getPreviousEncryption() {
904		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
905			if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
906				continue;
907			}
908			return iterator.getEncryption();
909		}
910		return ENCRYPTION_NONE;
911	}
912
913	private int getNextEncryption() {
914		if (this.conversation instanceof Conversation) {
915			Conversation conversation = (Conversation) this.conversation;
916			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
917				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
918					continue;
919				}
920				return iterator.getEncryption();
921			}
922			return conversation.getNextEncryption();
923		} else {
924			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
925		}
926	}
927
928	public boolean isValidInSession() {
929		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
930		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
931
932		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
933				|| futureEncryption == ENCRYPTION_NONE
934				|| pastEncryption != futureEncryption;
935
936		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
937	}
938
939	private static int getCleanedEncryption(int encryption) {
940		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
941			return ENCRYPTION_PGP;
942		}
943		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
944			return ENCRYPTION_AXOLOTL;
945		}
946		return encryption;
947	}
948
949	public static boolean configurePrivateMessage(final Message message) {
950		return configurePrivateMessage(message, false);
951	}
952
953	public static boolean configurePrivateFileMessage(final Message message) {
954		return configurePrivateMessage(message, true);
955	}
956
957	private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
958		final Conversation conversation;
959		if (message.conversation instanceof Conversation) {
960			conversation = (Conversation) message.conversation;
961		} else {
962			return false;
963		}
964		if (conversation.getMode() == Conversation.MODE_MULTI) {
965			final Jid nextCounterpart = conversation.getNextCounterpart();
966			if (nextCounterpart != null) {
967				message.setCounterpart(nextCounterpart);
968				message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
969				message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
970				return true;
971			}
972		}
973		return false;
974	}
975}