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