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.setStatus(Message.STATUS_RECEIVED);
204		message.body = body;
205		return message;
206	}
207
208	public static Message createLoadMoreMessage(Conversation conversation) {
209		final Message message = new Message(conversation);
210		message.setType(Message.TYPE_STATUS);
211		message.body = "LOAD_MORE";
212		return message;
213	}
214
215	public static Message createDateSeparator(Message message) {
216		final Message separator = new Message(message.getConversation());
217		separator.setType(Message.TYPE_STATUS);
218		separator.body = MessageAdapter.DATE_SEPARATOR_BODY;
219		separator.setTime(message.getTimeSent());
220		return separator;
221	}
222
223	@Override
224	public ContentValues getContentValues() {
225		ContentValues values = new ContentValues();
226		values.put(UUID, uuid);
227		values.put(CONVERSATION, conversationUuid);
228		if (counterpart == null) {
229			values.putNull(COUNTERPART);
230		} else {
231			values.put(COUNTERPART, counterpart.toPreppedString());
232		}
233		if (trueCounterpart == null) {
234			values.putNull(TRUE_COUNTERPART);
235		} else {
236			values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
237		}
238		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : body);
239		values.put(TIME_SENT, timeSent);
240		values.put(ENCRYPTION, encryption);
241		values.put(STATUS, status);
242		values.put(TYPE, type);
243		values.put(CARBON, carbon ? 1 : 0);
244		values.put(REMOTE_MSG_ID, remoteMsgId);
245		values.put(RELATIVE_FILE_PATH, relativeFilePath);
246		values.put(SERVER_MSG_ID, serverMsgId);
247		values.put(FINGERPRINT, axolotlFingerprint);
248		values.put(READ,read ? 1 : 0);
249		values.put(EDITED, edited);
250		values.put(OOB, oob ? 1 : 0);
251		values.put(ERROR_MESSAGE,errorMessage);
252		return values;
253	}
254
255	public String getConversationUuid() {
256		return conversationUuid;
257	}
258
259	public Conversation getConversation() {
260		return this.conversation;
261	}
262
263	public Jid getCounterpart() {
264		return counterpart;
265	}
266
267	public void setCounterpart(final Jid counterpart) {
268		this.counterpart = counterpart;
269	}
270
271	public Contact getContact() {
272		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
273			return this.conversation.getContact();
274		} else {
275			if (this.trueCounterpart == null) {
276				return null;
277			} else {
278				return this.conversation.getAccount().getRoster()
279						.getContactFromRoster(this.trueCounterpart);
280			}
281		}
282	}
283
284	public String getBody() {
285		return body;
286	}
287
288	public synchronized void setBody(String body) {
289		if (body == null) {
290			throw new Error("You should not set the message body to null");
291		}
292		this.body = body;
293		this.isGeoUri = null;
294		this.isEmojisOnly = null;
295		this.treatAsDownloadable = null;
296		this.fileParams = null;
297	}
298
299	public String getErrorMessage() {
300		return errorMessage;
301	}
302
303	public boolean setErrorMessage(String message) {
304		boolean changed = (message != null && !message.equals(errorMessage))
305				|| (message == null && errorMessage != null);
306		this.errorMessage = message;
307		return changed;
308	}
309
310	public long getTimeSent() {
311		return timeSent;
312	}
313
314	public int getEncryption() {
315		return encryption;
316	}
317
318	public void setEncryption(int encryption) {
319		this.encryption = encryption;
320	}
321
322	public int getStatus() {
323		return status;
324	}
325
326	public void setStatus(int status) {
327		this.status = status;
328	}
329
330	public String getRelativeFilePath() {
331		return this.relativeFilePath;
332	}
333
334	public void setRelativeFilePath(String path) {
335		this.relativeFilePath = path;
336	}
337
338	public String getRemoteMsgId() {
339		return this.remoteMsgId;
340	}
341
342	public void setRemoteMsgId(String id) {
343		this.remoteMsgId = id;
344	}
345
346	public String getServerMsgId() {
347		return this.serverMsgId;
348	}
349
350	public void setServerMsgId(String id) {
351		this.serverMsgId = id;
352	}
353
354	public boolean isRead() {
355		return this.read;
356	}
357
358	public void markRead() {
359		this.read = true;
360	}
361
362	public void markUnread() {
363		this.read = false;
364	}
365
366	public void setTime(long time) {
367		this.timeSent = time;
368	}
369
370	public String getEncryptedBody() {
371		return this.encryptedBody;
372	}
373
374	public void setEncryptedBody(String body) {
375		this.encryptedBody = body;
376	}
377
378	public int getType() {
379		return this.type;
380	}
381
382	public void setType(int type) {
383		this.type = type;
384	}
385
386	public boolean isCarbon() {
387		return carbon;
388	}
389
390	public void setCarbon(boolean carbon) {
391		this.carbon = carbon;
392	}
393
394	public void setEdited(String edited) {
395		this.edited = edited;
396	}
397
398	public boolean edited() {
399		return this.edited != null;
400	}
401
402	public void setTrueCounterpart(Jid trueCounterpart) {
403		this.trueCounterpart = trueCounterpart;
404	}
405
406	public Jid getTrueCounterpart() {
407		return this.trueCounterpart;
408	}
409
410	public Transferable getTransferable() {
411		return this.transferable;
412	}
413
414	public synchronized void setTransferable(Transferable transferable) {
415		this.fileParams = null;
416		this.transferable = transferable;
417	}
418
419	public boolean similar(Message message) {
420		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
421			return this.serverMsgId.equals(message.getServerMsgId());
422		} else if (this.body == null || this.counterpart == null) {
423			return false;
424		} else {
425			String body, otherBody;
426			if (this.hasFileOnRemoteHost()) {
427				body = getFileParams().url.toString();
428				otherBody = message.body == null ? null : message.body.trim();
429			} else {
430				body = this.body;
431				otherBody = message.body;
432			}
433			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
434			if (message.getRemoteMsgId() != null) {
435				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
436				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
437					return true;
438				}
439				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
440						&& matchingCounterpart
441						&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
442			} else {
443				return this.remoteMsgId == null
444						&& matchingCounterpart
445						&& body.equals(otherBody)
446						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
447			}
448		}
449	}
450
451	public Message next() {
452		synchronized (this.conversation.messages) {
453			if (this.mNextMessage == null) {
454				int index = this.conversation.messages.indexOf(this);
455				if (index < 0 || index >= this.conversation.messages.size() - 1) {
456					this.mNextMessage = null;
457				} else {
458					this.mNextMessage = this.conversation.messages.get(index + 1);
459				}
460			}
461			return this.mNextMessage;
462		}
463	}
464
465	public Message prev() {
466		synchronized (this.conversation.messages) {
467			if (this.mPreviousMessage == null) {
468				int index = this.conversation.messages.indexOf(this);
469				if (index <= 0 || index > this.conversation.messages.size()) {
470					this.mPreviousMessage = null;
471				} else {
472					this.mPreviousMessage = this.conversation.messages.get(index - 1);
473				}
474			}
475			return this.mPreviousMessage;
476		}
477	}
478
479	public boolean isLastCorrectableMessage() {
480		Message next = next();
481		while(next != null) {
482			if (next.isCorrectable()) {
483				return false;
484			}
485			next = next.next();
486		}
487		return isCorrectable();
488	}
489
490	private boolean isCorrectable() {
491		return getStatus() != STATUS_RECEIVED && !isCarbon();
492	}
493
494	public boolean mergeable(final Message message) {
495		return message != null &&
496				(message.getType() == Message.TYPE_TEXT &&
497						this.getTransferable() == null &&
498						message.getTransferable() == null &&
499						message.getEncryption() != Message.ENCRYPTION_PGP &&
500						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
501						this.getType() == message.getType() &&
502						//this.getStatus() == message.getStatus() &&
503						isStatusMergeable(this.getStatus(), message.getStatus()) &&
504						this.getEncryption() == message.getEncryption() &&
505						this.getCounterpart() != null &&
506						this.getCounterpart().equals(message.getCounterpart()) &&
507						this.edited() == message.edited() &&
508						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
509						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
510						!message.isGeoUri()&&
511						!this.isGeoUri() &&
512						!message.treatAsDownloadable() &&
513						!this.treatAsDownloadable() &&
514						!message.getBody().startsWith(ME_COMMAND) &&
515						!this.getBody().startsWith(ME_COMMAND) &&
516						!this.bodyIsOnlyEmojis() &&
517						!message.bodyIsOnlyEmojis() &&
518						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
519						UIHelper.sameDay(message.getTimeSent(),this.getTimeSent())
520				);
521	}
522
523	private static boolean isStatusMergeable(int a, int b) {
524		return a == b || (
525				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
526						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
527						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
528						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
529						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
530		);
531	}
532
533	public static class MergeSeparator {}
534
535	public SpannableStringBuilder getMergedBody() {
536		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
537		Message current = this;
538		while (current.mergeable(current.next())) {
539			current = current.next();
540			if (current == null) {
541				break;
542			}
543			body.append("\n\n");
544			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
545					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
546			body.append(current.getBody().trim());
547		}
548		return body;
549	}
550
551	public boolean hasMeCommand() {
552		return this.body.trim().startsWith(ME_COMMAND);
553	}
554
555	public int getMergedStatus() {
556		int status = this.status;
557		Message current = this;
558		while(current.mergeable(current.next())) {
559			current = current.next();
560			if (current == null) {
561				break;
562			}
563			status = current.status;
564		}
565		return status;
566	}
567
568	public long getMergedTimeSent() {
569		long time = this.timeSent;
570		Message current = this;
571		while(current.mergeable(current.next())) {
572			current = current.next();
573			if (current == null) {
574				break;
575			}
576			time = current.timeSent;
577		}
578		return time;
579	}
580
581	public boolean wasMergedIntoPrevious() {
582		Message prev = this.prev();
583		return prev != null && prev.mergeable(this);
584	}
585
586	public boolean trusted() {
587		Contact contact = this.getContact();
588		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
589	}
590
591	public boolean fixCounterpart() {
592		Presences presences = conversation.getContact().getPresences();
593		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
594			return true;
595		} else if (presences.size() >= 1) {
596			try {
597				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
598						conversation.getJid().getDomainpart(),
599						presences.toResourceArray()[0]);
600				return true;
601			} catch (InvalidJidException e) {
602				counterpart = null;
603				return false;
604			}
605		} else {
606			counterpart = null;
607			return false;
608		}
609	}
610
611	public void setUuid(String uuid) {
612		this.uuid = uuid;
613	}
614
615	public String getEditedId() {
616		return edited;
617	}
618
619	public void setOob(boolean isOob) {
620		this.oob = isOob;
621	}
622
623	private static String extractRelevantExtension(URL url) {
624		String path = url.getPath();
625		return extractRelevantExtension(path);
626	}
627
628	private static String extractRelevantExtension(String path) {
629		if (path == null || path.isEmpty()) {
630			return null;
631		}
632
633		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
634		int dotPosition = filename.lastIndexOf(".");
635
636		if (dotPosition != -1) {
637			String extension = filename.substring(dotPosition + 1);
638			// we want the real file extension, not the crypto one
639			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
640				return extractRelevantExtension(filename.substring(0,dotPosition));
641			} else {
642				return extension;
643			}
644		}
645		return null;
646	}
647
648	public String getMimeType() {
649		if (relativeFilePath != null) {
650			int start = relativeFilePath.lastIndexOf('.') + 1;
651			if (start < relativeFilePath.length()) {
652				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
653			} else {
654				return null;
655			}
656		} else {
657			try {
658				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
659			} catch (MalformedURLException e) {
660				return null;
661			}
662		}
663	}
664
665	public synchronized boolean treatAsDownloadable() {
666		if (treatAsDownloadable == null) {
667			if (body.trim().contains(" ")) {
668				treatAsDownloadable = false;
669			}
670			try {
671				final URL url = new URL(body);
672				final String ref = url.getRef();
673				final String protocol = url.getProtocol();
674				final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
675				treatAsDownloadable = (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
676						|| (("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted));
677
678			} catch (MalformedURLException e) {
679				treatAsDownloadable = false;
680			}
681		}
682		return treatAsDownloadable;
683	}
684
685	public synchronized boolean bodyIsOnlyEmojis() {
686		if (isEmojisOnly == null) {
687			isEmojisOnly = EmojiManager.isOnlyEmojis(body.replaceAll("\\s", ""));
688		}
689		return isEmojisOnly;
690	}
691
692	public synchronized boolean isGeoUri() {
693		if (isGeoUri == null) {
694			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
695		}
696		return isGeoUri;
697	}
698
699	public synchronized void resetFileParams() {
700		this.fileParams = null;
701	}
702
703	public synchronized FileParams getFileParams() {
704		if (fileParams == null) {
705			fileParams = new FileParams();
706			if (this.transferable != null) {
707				fileParams.size = this.transferable.getFileSize();
708			}
709			String parts[] = body == null ? new String[0] : body.split("\\|");
710			switch (parts.length) {
711				case 1:
712					try {
713						fileParams.size = Long.parseLong(parts[0]);
714					} catch (NumberFormatException e) {
715						fileParams.url = parseUrl(parts[0]);
716					}
717					break;
718				case 4:
719					fileParams.width = parseInt(parts[2]);
720					fileParams.height = parseInt(parts[3]);
721				case 2:
722					fileParams.url = parseUrl(parts[0]);
723					fileParams.size = parseLong(parts[1]);
724					break;
725				case 3:
726					fileParams.size = parseLong(parts[0]);
727					fileParams.width = parseInt(parts[1]);
728					fileParams.height = parseInt(parts[2]);
729					break;
730			}
731		}
732		return fileParams;
733	}
734
735	private static long parseLong(String value) {
736		try {
737			return Long.parseLong(value);
738		} catch (NumberFormatException e) {
739			return 0;
740		}
741	}
742
743	private static int parseInt(String value) {
744		try {
745			return Integer.parseInt(value);
746		} catch (NumberFormatException e) {
747			return 0;
748		}
749	}
750
751	private static URL parseUrl(String value) {
752		try {
753			return new URL(value);
754		} catch (MalformedURLException e) {
755			return null;
756		}
757	}
758
759	public void untie() {
760		this.mNextMessage = null;
761		this.mPreviousMessage = null;
762	}
763
764	public boolean isFileOrImage() {
765		return type == TYPE_FILE || type == TYPE_IMAGE;
766	}
767
768	public boolean hasFileOnRemoteHost() {
769		return isFileOrImage() && getFileParams().url != null;
770	}
771
772	public boolean needsUploading() {
773		return isFileOrImage() && getFileParams().url == null;
774	}
775
776	public class FileParams {
777		public URL url;
778		public long size = 0;
779		public int width = 0;
780		public int height = 0;
781	}
782
783	public void setFingerprint(String fingerprint) {
784		this.axolotlFingerprint = fingerprint;
785	}
786
787	public String getFingerprint() {
788		return axolotlFingerprint;
789	}
790
791	public boolean isTrusted() {
792		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
793		return s != null && s.isTrusted();
794	}
795
796	private  int getPreviousEncryption() {
797		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
798			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
799				continue;
800			}
801			return iterator.getEncryption();
802		}
803		return ENCRYPTION_NONE;
804	}
805
806	private int getNextEncryption() {
807		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
808			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
809				continue;
810			}
811			return iterator.getEncryption();
812		}
813		return conversation.getNextEncryption();
814	}
815
816	public boolean isValidInSession() {
817		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
818		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
819
820		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
821				|| futureEncryption == ENCRYPTION_NONE
822				|| pastEncryption != futureEncryption;
823
824		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
825	}
826
827	private static int getCleanedEncryption(int encryption) {
828		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
829			return ENCRYPTION_PGP;
830		}
831		return encryption;
832	}
833}