Message.java

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