Message.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.text.SpannableStringBuilder;
  6
  7import com.vdurmont.emoji.EmojiManager;
  8
  9import java.net.MalformedURLException;
 10import java.net.URL;
 11
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 14import eu.siacs.conversations.http.AesGcmURLStreamHandler;
 15import eu.siacs.conversations.ui.adapter.MessageAdapter;
 16import eu.siacs.conversations.utils.CryptoHelper;
 17import eu.siacs.conversations.utils.GeoHelper;
 18import eu.siacs.conversations.utils.MimeUtils;
 19import eu.siacs.conversations.utils.UIHelper;
 20import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 21import eu.siacs.conversations.xmpp.jid.Jid;
 22
 23public class Message extends AbstractEntity {
 24
 25	public static final String TABLENAME = "messages";
 26
 27	public static final int STATUS_RECEIVED = 0;
 28	public static final int STATUS_UNSEND = 1;
 29	public static final int STATUS_SEND = 2;
 30	public static final int STATUS_SEND_FAILED = 3;
 31	public static final int STATUS_WAITING = 5;
 32	public static final int STATUS_OFFERED = 6;
 33	public static final int STATUS_SEND_RECEIVED = 7;
 34	public static final int STATUS_SEND_DISPLAYED = 8;
 35
 36	public static final int ENCRYPTION_NONE = 0;
 37	public static final int ENCRYPTION_PGP = 1;
 38	public static final int ENCRYPTION_OTR = 2;
 39	public static final int ENCRYPTION_DECRYPTED = 3;
 40	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
 41	public static final int ENCRYPTION_AXOLOTL = 5;
 42
 43	public static final int TYPE_TEXT = 0;
 44	public static final int TYPE_IMAGE = 1;
 45	public static final int TYPE_FILE = 2;
 46	public static final int TYPE_STATUS = 3;
 47	public static final int TYPE_PRIVATE = 4;
 48
 49	public static final String CONVERSATION = "conversationUuid";
 50	public static final String COUNTERPART = "counterpart";
 51	public static final String TRUE_COUNTERPART = "trueCounterpart";
 52	public static final String BODY = "body";
 53	public static final String TIME_SENT = "timeSent";
 54	public static final String ENCRYPTION = "encryption";
 55	public static final String STATUS = "status";
 56	public static final String TYPE = "type";
 57	public static final String CARBON = "carbon";
 58	public static final String OOB = "oob";
 59	public static final String EDITED = "edited";
 60	public static final String REMOTE_MSG_ID = "remoteMsgId";
 61	public static final String SERVER_MSG_ID = "serverMsgId";
 62	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 63	public static final String FINGERPRINT = "axolotl_fingerprint";
 64	public static final String READ = "read";
 65	public static final String ERROR_MESSAGE = "errorMsg";
 66	public static final String ME_COMMAND = "/me ";
 67
 68
 69	public boolean markable = false;
 70	protected String conversationUuid;
 71	protected Jid counterpart;
 72	protected Jid trueCounterpart;
 73	protected String body;
 74	protected String encryptedBody;
 75	protected long timeSent;
 76	protected int encryption;
 77	protected int status;
 78	protected int type;
 79	protected boolean carbon = false;
 80	protected boolean oob = false;
 81	protected String edited = null;
 82	protected String relativeFilePath;
 83	protected boolean read = true;
 84	protected String remoteMsgId = null;
 85	protected String serverMsgId = null;
 86	private final Conversation conversation;
 87	protected Transferable transferable = null;
 88	private Message mNextMessage = null;
 89	private Message mPreviousMessage = null;
 90	private String axolotlFingerprint = null;
 91	private String errorMessage = null;
 92
 93	private Boolean isGeoUri = null;
 94	private Boolean isEmojisOnly = null;
 95	private Boolean treatAsDownloadable = null;
 96	private FileParams fileParams = null;
 97
 98	private Message(Conversation conversation) {
 99		this.conversation = conversation;
100	}
101
102	public Message(Conversation conversation, String body, int encryption) {
103		this(conversation, body, encryption, STATUS_UNSEND);
104	}
105
106	public Message(Conversation conversation, String body, int encryption, int status) {
107		this(conversation, java.util.UUID.randomUUID().toString(),
108				conversation.getUuid(),
109				conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
110				null,
111				body,
112				System.currentTimeMillis(),
113				encryption,
114				status,
115				TYPE_TEXT,
116				false,
117				null,
118				null,
119				null,
120				null,
121				true,
122				null,
123				false,
124				null);
125	}
126
127	private Message(final Conversation conversation, final String uuid, final String conversationUUid, final Jid counterpart,
128					final Jid trueCounterpart, final String body, final long timeSent,
129					final int encryption, final int status, final int type, final boolean carbon,
130					final String remoteMsgId, final String relativeFilePath,
131					final String serverMsgId, final String fingerprint, final boolean read,
132					final String edited, final boolean oob, final String errorMessage) {
133		this.conversation = conversation;
134		this.uuid = uuid;
135		this.conversationUuid = conversationUUid;
136		this.counterpart = counterpart;
137		this.trueCounterpart = trueCounterpart;
138		this.body = body == null ? "" : body;
139		this.timeSent = timeSent;
140		this.encryption = encryption;
141		this.status = status;
142		this.type = type;
143		this.carbon = carbon;
144		this.remoteMsgId = remoteMsgId;
145		this.relativeFilePath = relativeFilePath;
146		this.serverMsgId = serverMsgId;
147		this.axolotlFingerprint = fingerprint;
148		this.read = read;
149		this.edited = edited;
150		this.oob = oob;
151		this.errorMessage = errorMessage;
152	}
153
154	public static Message fromCursor(Cursor cursor, Conversation conversation) {
155		Jid jid;
156		try {
157			String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
158			if (value != null) {
159				jid = Jid.fromString(value, true);
160			} else {
161				jid = null;
162			}
163		} catch (InvalidJidException e) {
164			jid = null;
165		} catch (IllegalStateException e) {
166			return null; // message too long?
167		}
168		Jid trueCounterpart;
169		try {
170			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
171			if (value != null) {
172				trueCounterpart = Jid.fromString(value, true);
173			} else {
174				trueCounterpart = null;
175			}
176		} catch (InvalidJidException e) {
177			trueCounterpart = null;
178		}
179		return new Message(conversation,
180				cursor.getString(cursor.getColumnIndex(UUID)),
181				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
182				jid,
183				trueCounterpart,
184				cursor.getString(cursor.getColumnIndex(BODY)),
185				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
186				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
187				cursor.getInt(cursor.getColumnIndex(STATUS)),
188				cursor.getInt(cursor.getColumnIndex(TYPE)),
189				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
190				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
191				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
192				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
193				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
194				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
195				cursor.getString(cursor.getColumnIndex(EDITED)),
196				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
197				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
198	}
199
200	public static Message createStatusMessage(Conversation conversation, String body) {
201		final Message message = new Message(conversation);
202		message.setType(Message.TYPE_STATUS);
203		message.body = body;
204		return message;
205	}
206
207	public static Message createLoadMoreMessage(Conversation conversation) {
208		final Message message = new Message(conversation);
209		message.setType(Message.TYPE_STATUS);
210		message.body = "LOAD_MORE";
211		return message;
212	}
213
214	public static Message createDateSeparator(Message message) {
215		final Message separator = new Message(message.getConversation());
216		separator.setType(Message.TYPE_STATUS);
217		separator.body = MessageAdapter.DATE_SEPARATOR_BODY;
218		separator.setTime(message.getTimeSent());
219		return separator;
220	}
221
222	@Override
223	public ContentValues getContentValues() {
224		ContentValues values = new ContentValues();
225		values.put(UUID, uuid);
226		values.put(CONVERSATION, conversationUuid);
227		if (counterpart == null) {
228			values.putNull(COUNTERPART);
229		} else {
230			values.put(COUNTERPART, counterpart.toPreppedString());
231		}
232		if (trueCounterpart == null) {
233			values.putNull(TRUE_COUNTERPART);
234		} else {
235			values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
236		}
237		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : body);
238		values.put(TIME_SENT, timeSent);
239		values.put(ENCRYPTION, encryption);
240		values.put(STATUS, status);
241		values.put(TYPE, type);
242		values.put(CARBON, carbon ? 1 : 0);
243		values.put(REMOTE_MSG_ID, remoteMsgId);
244		values.put(RELATIVE_FILE_PATH, relativeFilePath);
245		values.put(SERVER_MSG_ID, serverMsgId);
246		values.put(FINGERPRINT, axolotlFingerprint);
247		values.put(READ,read ? 1 : 0);
248		values.put(EDITED, edited);
249		values.put(OOB, oob ? 1 : 0);
250		values.put(ERROR_MESSAGE,errorMessage);
251		return values;
252	}
253
254	public String getConversationUuid() {
255		return conversationUuid;
256	}
257
258	public Conversation getConversation() {
259		return this.conversation;
260	}
261
262	public Jid getCounterpart() {
263		return counterpart;
264	}
265
266	public void setCounterpart(final Jid counterpart) {
267		this.counterpart = counterpart;
268	}
269
270	public Contact getContact() {
271		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
272			return this.conversation.getContact();
273		} else {
274			if (this.trueCounterpart == null) {
275				return null;
276			} else {
277				return this.conversation.getAccount().getRoster()
278						.getContactFromRoster(this.trueCounterpart);
279			}
280		}
281	}
282
283	public String getBody() {
284		return body;
285	}
286
287	public synchronized void setBody(String body) {
288		if (body == null) {
289			throw new Error("You should not set the message body to null");
290		}
291		this.body = body;
292		this.isGeoUri = null;
293		this.isEmojisOnly = null;
294		this.treatAsDownloadable = null;
295		this.fileParams = null;
296	}
297
298	public String getErrorMessage() {
299		return errorMessage;
300	}
301
302	public boolean setErrorMessage(String message) {
303		boolean changed = (message != null && !message.equals(errorMessage))
304				|| (message == null && errorMessage != null);
305		this.errorMessage = message;
306		return changed;
307	}
308
309	public long getTimeSent() {
310		return timeSent;
311	}
312
313	public int getEncryption() {
314		return encryption;
315	}
316
317	public void setEncryption(int encryption) {
318		this.encryption = encryption;
319	}
320
321	public int getStatus() {
322		return status;
323	}
324
325	public void setStatus(int status) {
326		this.status = status;
327	}
328
329	public String getRelativeFilePath() {
330		return this.relativeFilePath;
331	}
332
333	public void setRelativeFilePath(String path) {
334		this.relativeFilePath = path;
335	}
336
337	public String getRemoteMsgId() {
338		return this.remoteMsgId;
339	}
340
341	public void setRemoteMsgId(String id) {
342		this.remoteMsgId = id;
343	}
344
345	public String getServerMsgId() {
346		return this.serverMsgId;
347	}
348
349	public void setServerMsgId(String id) {
350		this.serverMsgId = id;
351	}
352
353	public boolean isRead() {
354		return this.read;
355	}
356
357	public void markRead() {
358		this.read = true;
359	}
360
361	public void markUnread() {
362		this.read = false;
363	}
364
365	public void setTime(long time) {
366		this.timeSent = time;
367	}
368
369	public String getEncryptedBody() {
370		return this.encryptedBody;
371	}
372
373	public void setEncryptedBody(String body) {
374		this.encryptedBody = body;
375	}
376
377	public int getType() {
378		return this.type;
379	}
380
381	public void setType(int type) {
382		this.type = type;
383	}
384
385	public boolean isCarbon() {
386		return carbon;
387	}
388
389	public void setCarbon(boolean carbon) {
390		this.carbon = carbon;
391	}
392
393	public void setEdited(String edited) {
394		this.edited = edited;
395	}
396
397	public boolean edited() {
398		return this.edited != null;
399	}
400
401	public void setTrueCounterpart(Jid trueCounterpart) {
402		this.trueCounterpart = trueCounterpart;
403	}
404
405	public Jid getTrueCounterpart() {
406		return this.trueCounterpart;
407	}
408
409	public Transferable getTransferable() {
410		return this.transferable;
411	}
412
413	public synchronized void setTransferable(Transferable transferable) {
414		this.fileParams = null;
415		this.transferable = transferable;
416	}
417
418	public boolean similar(Message message) {
419		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
420			return this.serverMsgId.equals(message.getServerMsgId());
421		} else if (this.body == null || this.counterpart == null) {
422			return false;
423		} else {
424			String body, otherBody;
425			if (this.hasFileOnRemoteHost()) {
426				body = getFileParams().url.toString();
427				otherBody = message.body == null ? null : message.body.trim();
428			} else {
429				body = this.body;
430				otherBody = message.body;
431			}
432			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
433			if (message.getRemoteMsgId() != null) {
434				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
435				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
436					return true;
437				}
438				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
439						&& matchingCounterpart
440						&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
441			} else {
442				return this.remoteMsgId == null
443						&& matchingCounterpart
444						&& body.equals(otherBody)
445						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
446			}
447		}
448	}
449
450	public Message next() {
451		synchronized (this.conversation.messages) {
452			if (this.mNextMessage == null) {
453				int index = this.conversation.messages.indexOf(this);
454				if (index < 0 || index >= this.conversation.messages.size() - 1) {
455					this.mNextMessage = null;
456				} else {
457					this.mNextMessage = this.conversation.messages.get(index + 1);
458				}
459			}
460			return this.mNextMessage;
461		}
462	}
463
464	public Message prev() {
465		synchronized (this.conversation.messages) {
466			if (this.mPreviousMessage == null) {
467				int index = this.conversation.messages.indexOf(this);
468				if (index <= 0 || index > this.conversation.messages.size()) {
469					this.mPreviousMessage = null;
470				} else {
471					this.mPreviousMessage = this.conversation.messages.get(index - 1);
472				}
473			}
474			return this.mPreviousMessage;
475		}
476	}
477
478	public boolean isLastCorrectableMessage() {
479		Message next = next();
480		while(next != null) {
481			if (next.isCorrectable()) {
482				return false;
483			}
484			next = next.next();
485		}
486		return isCorrectable();
487	}
488
489	private boolean isCorrectable() {
490		return getStatus() != STATUS_RECEIVED && !isCarbon();
491	}
492
493	public boolean mergeable(final Message message) {
494		return message != null &&
495				(message.getType() == Message.TYPE_TEXT &&
496						this.getTransferable() == null &&
497						message.getTransferable() == null &&
498						message.getEncryption() != Message.ENCRYPTION_PGP &&
499						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
500						this.getType() == message.getType() &&
501						//this.getStatus() == message.getStatus() &&
502						isStatusMergeable(this.getStatus(), message.getStatus()) &&
503						this.getEncryption() == message.getEncryption() &&
504						this.getCounterpart() != null &&
505						this.getCounterpart().equals(message.getCounterpart()) &&
506						this.edited() == message.edited() &&
507						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
508						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
509						!message.isGeoUri()&&
510						!this.isGeoUri() &&
511						!message.treatAsDownloadable() &&
512						!this.treatAsDownloadable() &&
513						!message.getBody().startsWith(ME_COMMAND) &&
514						!this.getBody().startsWith(ME_COMMAND) &&
515						!this.bodyIsOnlyEmojis() &&
516						!message.bodyIsOnlyEmojis() &&
517						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
518						UIHelper.sameDay(message.getTimeSent(),this.getTimeSent())
519				);
520	}
521
522	private static boolean isStatusMergeable(int a, int b) {
523		return a == b || (
524				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
525						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
526						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
527						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
528						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
529		);
530	}
531
532	public static class MergeSeparator {}
533
534	public SpannableStringBuilder getMergedBody() {
535		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
536		Message current = this;
537		while (current.mergeable(current.next())) {
538			current = current.next();
539			if (current == null) {
540				break;
541			}
542			body.append("\n\n");
543			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
544					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
545			body.append(current.getBody().trim());
546		}
547		return body;
548	}
549
550	public boolean hasMeCommand() {
551		return this.body.trim().startsWith(ME_COMMAND);
552	}
553
554	public int getMergedStatus() {
555		int status = this.status;
556		Message current = this;
557		while(current.mergeable(current.next())) {
558			current = current.next();
559			if (current == null) {
560				break;
561			}
562			status = current.status;
563		}
564		return status;
565	}
566
567	public long getMergedTimeSent() {
568		long time = this.timeSent;
569		Message current = this;
570		while(current.mergeable(current.next())) {
571			current = current.next();
572			if (current == null) {
573				break;
574			}
575			time = current.timeSent;
576		}
577		return time;
578	}
579
580	public boolean wasMergedIntoPrevious() {
581		Message prev = this.prev();
582		return prev != null && prev.mergeable(this);
583	}
584
585	public boolean trusted() {
586		Contact contact = this.getContact();
587		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
588	}
589
590	public boolean fixCounterpart() {
591		Presences presences = conversation.getContact().getPresences();
592		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
593			return true;
594		} else if (presences.size() >= 1) {
595			try {
596				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
597						conversation.getJid().getDomainpart(),
598						presences.toResourceArray()[0]);
599				return true;
600			} catch (InvalidJidException e) {
601				counterpart = null;
602				return false;
603			}
604		} else {
605			counterpart = null;
606			return false;
607		}
608	}
609
610	public void setUuid(String uuid) {
611		this.uuid = uuid;
612	}
613
614	public String getEditedId() {
615		return edited;
616	}
617
618	public void setOob(boolean isOob) {
619		this.oob = isOob;
620	}
621
622	private static String extractRelevantExtension(URL url) {
623		String path = url.getPath();
624		return extractRelevantExtension(path);
625	}
626
627	private static String extractRelevantExtension(String path) {
628		if (path == null || path.isEmpty()) {
629			return null;
630		}
631
632		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
633		int dotPosition = filename.lastIndexOf(".");
634
635		if (dotPosition != -1) {
636			String extension = filename.substring(dotPosition + 1);
637			// we want the real file extension, not the crypto one
638			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
639				return extractRelevantExtension(filename.substring(0,dotPosition));
640			} else {
641				return extension;
642			}
643		}
644		return null;
645	}
646
647	public String getMimeType() {
648		if (relativeFilePath != null) {
649			int start = relativeFilePath.lastIndexOf('.') + 1;
650			if (start < relativeFilePath.length()) {
651				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
652			} else {
653				return null;
654			}
655		} else {
656			try {
657				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
658			} catch (MalformedURLException e) {
659				return null;
660			}
661		}
662	}
663
664	public synchronized boolean treatAsDownloadable() {
665		if (treatAsDownloadable == null) {
666			if (body.trim().contains(" ")) {
667				treatAsDownloadable = false;
668			}
669			try {
670				final URL url = new URL(body);
671				final String ref = url.getRef();
672				final String protocol = url.getProtocol();
673				final boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
674				treatAsDownloadable = (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
675						|| (("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted));
676
677			} catch (MalformedURLException e) {
678				treatAsDownloadable = false;
679			}
680		}
681		return treatAsDownloadable;
682	}
683
684	public synchronized boolean bodyIsOnlyEmojis() {
685		if (isEmojisOnly == null) {
686			isEmojisOnly = EmojiManager.isOnlyEmojis(body.replaceAll("\\s", ""));
687		}
688		return isEmojisOnly;
689	}
690
691	public synchronized boolean isGeoUri() {
692		if (isGeoUri == null) {
693			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
694		}
695		return isGeoUri;
696	}
697
698	public synchronized FileParams getFileParams() {
699		if (fileParams == null) {
700			fileParams = new FileParams();
701			if (this.transferable != null) {
702				fileParams.size = this.transferable.getFileSize();
703			}
704			String parts[] = body == null ? new String[0] : body.split("\\|");
705			switch (parts.length) {
706				case 1:
707					try {
708						fileParams.size = Long.parseLong(parts[0]);
709					} catch (NumberFormatException e) {
710						fileParams.url = parseUrl(parts[0]);
711					}
712					break;
713				case 4:
714					fileParams.width = parseInt(parts[2]);
715					fileParams.height = parseInt(parts[3]);
716				case 2:
717					fileParams.url = parseUrl(parts[0]);
718					fileParams.size = parseLong(parts[1]);
719					break;
720				case 3:
721					fileParams.size = parseLong(parts[0]);
722					fileParams.width = parseInt(parts[1]);
723					fileParams.height = parseInt(parts[2]);
724					break;
725			}
726		}
727		return fileParams;
728	}
729
730	private static long parseLong(String value) {
731		try {
732			return Long.parseLong(value);
733		} catch (NumberFormatException e) {
734			return 0;
735		}
736	}
737
738	private static int parseInt(String value) {
739		try {
740			return Integer.parseInt(value);
741		} catch (NumberFormatException e) {
742			return 0;
743		}
744	}
745
746	private static URL parseUrl(String value) {
747		try {
748			return new URL(value);
749		} catch (MalformedURLException e) {
750			return null;
751		}
752	}
753
754	public void untie() {
755		this.mNextMessage = null;
756		this.mPreviousMessage = null;
757	}
758
759	public boolean isFileOrImage() {
760		return type == TYPE_FILE || type == TYPE_IMAGE;
761	}
762
763	public boolean hasFileOnRemoteHost() {
764		return isFileOrImage() && getFileParams().url != null;
765	}
766
767	public boolean needsUploading() {
768		return isFileOrImage() && getFileParams().url == null;
769	}
770
771	public class FileParams {
772		public URL url;
773		public long size = 0;
774		public int width = 0;
775		public int height = 0;
776	}
777
778	public void setFingerprint(String fingerprint) {
779		this.axolotlFingerprint = fingerprint;
780	}
781
782	public String getFingerprint() {
783		return axolotlFingerprint;
784	}
785
786	public boolean isTrusted() {
787		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
788		return s != null && s.isTrusted();
789	}
790
791	private  int getPreviousEncryption() {
792		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
793			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
794				continue;
795			}
796			return iterator.getEncryption();
797		}
798		return ENCRYPTION_NONE;
799	}
800
801	private int getNextEncryption() {
802		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
803			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
804				continue;
805			}
806			return iterator.getEncryption();
807		}
808		return conversation.getNextEncryption();
809	}
810
811	public boolean isValidInSession() {
812		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
813		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
814
815		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
816				|| futureEncryption == ENCRYPTION_NONE
817				|| pastEncryption != futureEncryption;
818
819		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
820	}
821
822	private static int getCleanedEncryption(int encryption) {
823		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
824			return ENCRYPTION_PGP;
825		}
826		return encryption;
827	}
828}