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.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.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : 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			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			if (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted) {
664				return Decision.MUST;
665			}
666			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
667				return Decision.NEVER;
668			} else if (oob) {
669				return Decision.MUST;
670			}
671			String extension = extractRelevantExtension(url);
672			if (extension == null) {
673				return Decision.NEVER;
674			}
675
676			if (encrypted) {
677				return Decision.MUST;
678			} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
679					|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
680				return Decision.SHOULD;
681			} else {
682				return Decision.NEVER;
683			}
684
685		} catch (MalformedURLException e) {
686			return Decision.NEVER;
687		}
688	}
689
690	public boolean bodyIsHeart() {
691		return body != null && UIHelper.HEARTS.contains(body.trim());
692	}
693
694	public FileParams getFileParams() {
695		FileParams params = getLegacyFileParams();
696		if (params != null) {
697			return params;
698		}
699		params = new FileParams();
700		if (this.transferable != null) {
701			params.size = this.transferable.getFileSize();
702		}
703		if (body == null) {
704			return params;
705		}
706		String parts[] = body.split("\\|");
707		switch (parts.length) {
708			case 1:
709				try {
710					params.size = Long.parseLong(parts[0]);
711				} catch (NumberFormatException e) {
712					try {
713						params.url = new URL(parts[0]);
714					} catch (MalformedURLException e1) {
715						params.url = null;
716					}
717				}
718				break;
719			case 2:
720			case 4:
721				try {
722					params.url = new URL(parts[0]);
723				} catch (MalformedURLException e1) {
724					params.url = null;
725				}
726				try {
727					params.size = Long.parseLong(parts[1]);
728				} catch (NumberFormatException e) {
729					params.size = 0;
730				}
731				try {
732					params.width = Integer.parseInt(parts[2]);
733				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
734					params.width = 0;
735				}
736				try {
737					params.height = Integer.parseInt(parts[3]);
738				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
739					params.height = 0;
740				}
741				break;
742			case 3:
743				try {
744					params.size = Long.parseLong(parts[0]);
745				} catch (NumberFormatException e) {
746					params.size = 0;
747				}
748				try {
749					params.width = Integer.parseInt(parts[1]);
750				} catch (NumberFormatException e) {
751					params.width = 0;
752				}
753				try {
754					params.height = Integer.parseInt(parts[2]);
755				} catch (NumberFormatException e) {
756					params.height = 0;
757				}
758				break;
759		}
760		return params;
761	}
762
763	public FileParams getLegacyFileParams() {
764		FileParams params = new FileParams();
765		if (body == null) {
766			return params;
767		}
768		String parts[] = body.split(",");
769		if (parts.length == 3) {
770			try {
771				params.size = Long.parseLong(parts[0]);
772			} catch (NumberFormatException e) {
773				return null;
774			}
775			try {
776				params.width = Integer.parseInt(parts[1]);
777			} catch (NumberFormatException e) {
778				return null;
779			}
780			try {
781				params.height = Integer.parseInt(parts[2]);
782			} catch (NumberFormatException e) {
783				return null;
784			}
785			return params;
786		} else {
787			return null;
788		}
789	}
790
791	public void untie() {
792		this.mNextMessage = null;
793		this.mPreviousMessage = null;
794	}
795
796	public boolean isFileOrImage() {
797		return type == TYPE_FILE || type == TYPE_IMAGE;
798	}
799
800	public boolean hasFileOnRemoteHost() {
801		return isFileOrImage() && getFileParams().url != null;
802	}
803
804	public boolean needsUploading() {
805		return isFileOrImage() && getFileParams().url == null;
806	}
807
808	public class FileParams {
809		public URL url;
810		public long size = 0;
811		public int width = 0;
812		public int height = 0;
813	}
814
815	public void setFingerprint(String fingerprint) {
816		this.axolotlFingerprint = fingerprint;
817	}
818
819	public String getFingerprint() {
820		return axolotlFingerprint;
821	}
822
823	public boolean isTrusted() {
824		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
825		return s != null && s.isTrusted();
826	}
827
828	private  int getPreviousEncryption() {
829		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
830			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
831				continue;
832			}
833			return iterator.getEncryption();
834		}
835		return ENCRYPTION_NONE;
836	}
837
838	private int getNextEncryption() {
839		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
840			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
841				continue;
842			}
843			return iterator.getEncryption();
844		}
845		return conversation.getNextEncryption();
846	}
847
848	public boolean isValidInSession() {
849		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
850		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
851
852		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
853				|| futureEncryption == ENCRYPTION_NONE
854				|| pastEncryption != futureEncryption;
855
856		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
857	}
858
859	private static int getCleanedEncryption(int encryption) {
860		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
861			return ENCRYPTION_PGP;
862		}
863		return encryption;
864	}
865}