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 void resetFileParams() {
699		this.fileParams = null;
700	}
701
702	public synchronized FileParams getFileParams() {
703		if (fileParams == null) {
704			fileParams = new FileParams();
705			if (this.transferable != null) {
706				fileParams.size = this.transferable.getFileSize();
707			}
708			String parts[] = body == null ? new String[0] : body.split("\\|");
709			switch (parts.length) {
710				case 1:
711					try {
712						fileParams.size = Long.parseLong(parts[0]);
713					} catch (NumberFormatException e) {
714						fileParams.url = parseUrl(parts[0]);
715					}
716					break;
717				case 4:
718					fileParams.width = parseInt(parts[2]);
719					fileParams.height = parseInt(parts[3]);
720				case 2:
721					fileParams.url = parseUrl(parts[0]);
722					fileParams.size = parseLong(parts[1]);
723					break;
724				case 3:
725					fileParams.size = parseLong(parts[0]);
726					fileParams.width = parseInt(parts[1]);
727					fileParams.height = parseInt(parts[2]);
728					break;
729			}
730		}
731		return fileParams;
732	}
733
734	private static long parseLong(String value) {
735		try {
736			return Long.parseLong(value);
737		} catch (NumberFormatException e) {
738			return 0;
739		}
740	}
741
742	private static int parseInt(String value) {
743		try {
744			return Integer.parseInt(value);
745		} catch (NumberFormatException e) {
746			return 0;
747		}
748	}
749
750	private static URL parseUrl(String value) {
751		try {
752			return new URL(value);
753		} catch (MalformedURLException e) {
754			return null;
755		}
756	}
757
758	public void untie() {
759		this.mNextMessage = null;
760		this.mPreviousMessage = null;
761	}
762
763	public boolean isFileOrImage() {
764		return type == TYPE_FILE || type == TYPE_IMAGE;
765	}
766
767	public boolean hasFileOnRemoteHost() {
768		return isFileOrImage() && getFileParams().url != null;
769	}
770
771	public boolean needsUploading() {
772		return isFileOrImage() && getFileParams().url == null;
773	}
774
775	public class FileParams {
776		public URL url;
777		public long size = 0;
778		public int width = 0;
779		public int height = 0;
780	}
781
782	public void setFingerprint(String fingerprint) {
783		this.axolotlFingerprint = fingerprint;
784	}
785
786	public String getFingerprint() {
787		return axolotlFingerprint;
788	}
789
790	public boolean isTrusted() {
791		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
792		return s != null && s.isTrusted();
793	}
794
795	private  int getPreviousEncryption() {
796		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
797			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
798				continue;
799			}
800			return iterator.getEncryption();
801		}
802		return ENCRYPTION_NONE;
803	}
804
805	private int getNextEncryption() {
806		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
807			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
808				continue;
809			}
810			return iterator.getEncryption();
811		}
812		return conversation.getNextEncryption();
813	}
814
815	public boolean isValidInSession() {
816		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
817		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
818
819		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
820				|| futureEncryption == ENCRYPTION_NONE
821				|| pastEncryption != futureEncryption;
822
823		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
824	}
825
826	private static int getCleanedEncryption(int encryption) {
827		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
828			return ENCRYPTION_PGP;
829		}
830		return encryption;
831	}
832}