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() &&
496						!this.treatAsDownloadable() &&
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	private static String extractRelevantExtension(URL url) {
607		String path = url.getPath();
608		return extractRelevantExtension(path);
609	}
610
611	private static String extractRelevantExtension(String path) {
612		if (path == null || path.isEmpty()) {
613			return null;
614		}
615
616		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
617		int dotPosition = filename.lastIndexOf(".");
618
619		if (dotPosition != -1) {
620			String extension = filename.substring(dotPosition + 1);
621			// we want the real file extension, not the crypto one
622			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
623				return extractRelevantExtension(filename.substring(0,dotPosition));
624			} else {
625				return extension;
626			}
627		}
628		return null;
629	}
630
631	public String getMimeType() {
632		if (relativeFilePath != null) {
633			int start = relativeFilePath.lastIndexOf('.') + 1;
634			if (start < relativeFilePath.length()) {
635				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
636			} else {
637				return null;
638			}
639		} else {
640			try {
641				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
642			} catch (MalformedURLException e) {
643				return null;
644			}
645		}
646	}
647
648	public boolean treatAsDownloadable() {
649		if (body.trim().contains(" ")) {
650			return false;
651		}
652		try {
653			final URL url = new URL(body);
654			final String ref = url.getRef();
655			final String protocol = url.getProtocol();
656			final boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
657			return (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
658					|| ((protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) && (oob || encrypted));
659
660		} catch (MalformedURLException e) {
661			return false;
662		}
663	}
664
665	public boolean bodyIsHeart() {
666		return body != null && UIHelper.HEARTS.contains(body.trim());
667	}
668
669	public FileParams getFileParams() {
670		FileParams params = getLegacyFileParams();
671		if (params != null) {
672			return params;
673		}
674		params = new FileParams();
675		if (this.transferable != null) {
676			params.size = this.transferable.getFileSize();
677		}
678		if (body == null) {
679			return params;
680		}
681		String parts[] = body.split("\\|");
682		switch (parts.length) {
683			case 1:
684				try {
685					params.size = Long.parseLong(parts[0]);
686				} catch (NumberFormatException e) {
687					try {
688						params.url = new URL(parts[0]);
689					} catch (MalformedURLException e1) {
690						params.url = null;
691					}
692				}
693				break;
694			case 2:
695			case 4:
696				try {
697					params.url = new URL(parts[0]);
698				} catch (MalformedURLException e1) {
699					params.url = null;
700				}
701				try {
702					params.size = Long.parseLong(parts[1]);
703				} catch (NumberFormatException e) {
704					params.size = 0;
705				}
706				try {
707					params.width = Integer.parseInt(parts[2]);
708				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
709					params.width = 0;
710				}
711				try {
712					params.height = Integer.parseInt(parts[3]);
713				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
714					params.height = 0;
715				}
716				break;
717			case 3:
718				try {
719					params.size = Long.parseLong(parts[0]);
720				} catch (NumberFormatException e) {
721					params.size = 0;
722				}
723				try {
724					params.width = Integer.parseInt(parts[1]);
725				} catch (NumberFormatException e) {
726					params.width = 0;
727				}
728				try {
729					params.height = Integer.parseInt(parts[2]);
730				} catch (NumberFormatException e) {
731					params.height = 0;
732				}
733				break;
734		}
735		return params;
736	}
737
738	public FileParams getLegacyFileParams() {
739		FileParams params = new FileParams();
740		if (body == null) {
741			return params;
742		}
743		String parts[] = body.split(",");
744		if (parts.length == 3) {
745			try {
746				params.size = Long.parseLong(parts[0]);
747			} catch (NumberFormatException e) {
748				return null;
749			}
750			try {
751				params.width = Integer.parseInt(parts[1]);
752			} catch (NumberFormatException e) {
753				return null;
754			}
755			try {
756				params.height = Integer.parseInt(parts[2]);
757			} catch (NumberFormatException e) {
758				return null;
759			}
760			return params;
761		} else {
762			return null;
763		}
764	}
765
766	public void untie() {
767		this.mNextMessage = null;
768		this.mPreviousMessage = null;
769	}
770
771	public boolean isFileOrImage() {
772		return type == TYPE_FILE || type == TYPE_IMAGE;
773	}
774
775	public boolean hasFileOnRemoteHost() {
776		return isFileOrImage() && getFileParams().url != null;
777	}
778
779	public boolean needsUploading() {
780		return isFileOrImage() && getFileParams().url == null;
781	}
782
783	public class FileParams {
784		public URL url;
785		public long size = 0;
786		public int width = 0;
787		public int height = 0;
788	}
789
790	public void setFingerprint(String fingerprint) {
791		this.axolotlFingerprint = fingerprint;
792	}
793
794	public String getFingerprint() {
795		return axolotlFingerprint;
796	}
797
798	public boolean isTrusted() {
799		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
800		return s != null && s.isTrusted();
801	}
802
803	private  int getPreviousEncryption() {
804		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
805			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
806				continue;
807			}
808			return iterator.getEncryption();
809		}
810		return ENCRYPTION_NONE;
811	}
812
813	private int getNextEncryption() {
814		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
815			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
816				continue;
817			}
818			return iterator.getEncryption();
819		}
820		return conversation.getNextEncryption();
821	}
822
823	public boolean isValidInSession() {
824		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
825		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
826
827		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
828				|| futureEncryption == ENCRYPTION_NONE
829				|| pastEncryption != futureEncryption;
830
831		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
832	}
833
834	private static int getCleanedEncryption(int encryption) {
835		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
836			return ENCRYPTION_PGP;
837		}
838		return encryption;
839	}
840}