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