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