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_SEND_RECEIVED && b == Message.STATUS_WAITING)
510						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
511						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
512		);
513	}
514
515	public static class MergeSeparator {}
516
517	public SpannableStringBuilder getMergedBody() {
518		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
519		Message current = this;
520		while (current.mergeable(current.next())) {
521			current = current.next();
522			if (current == null) {
523				break;
524			}
525			body.append("\n\n");
526			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
527					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
528			body.append(current.getBody().trim());
529		}
530		return body;
531	}
532
533	public boolean hasMeCommand() {
534		return this.body.trim().startsWith(ME_COMMAND);
535	}
536
537	public int getMergedStatus() {
538		int status = this.status;
539		Message current = this;
540		while(current.mergeable(current.next())) {
541			current = current.next();
542			if (current == null) {
543				break;
544			}
545			status = current.status;
546		}
547		return status;
548	}
549
550	public long getMergedTimeSent() {
551		long time = this.timeSent;
552		Message current = this;
553		while(current.mergeable(current.next())) {
554			current = current.next();
555			if (current == null) {
556				break;
557			}
558			time = current.timeSent;
559		}
560		return time;
561	}
562
563	public boolean wasMergedIntoPrevious() {
564		Message prev = this.prev();
565		return prev != null && prev.mergeable(this);
566	}
567
568	public boolean trusted() {
569		Contact contact = this.getContact();
570		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
571	}
572
573	public boolean fixCounterpart() {
574		Presences presences = conversation.getContact().getPresences();
575		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
576			return true;
577		} else if (presences.size() >= 1) {
578			try {
579				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
580						conversation.getJid().getDomainpart(),
581						presences.toResourceArray()[0]);
582				return true;
583			} catch (InvalidJidException e) {
584				counterpart = null;
585				return false;
586			}
587		} else {
588			counterpart = null;
589			return false;
590		}
591	}
592
593	public void setUuid(String uuid) {
594		this.uuid = uuid;
595	}
596
597	public String getEditedId() {
598		return edited;
599	}
600
601	public void setOob(boolean isOob) {
602		this.oob = isOob;
603	}
604
605	private static String extractRelevantExtension(URL url) {
606		String path = url.getPath();
607		return extractRelevantExtension(path);
608	}
609
610	private static String extractRelevantExtension(String path) {
611		if (path == null || path.isEmpty()) {
612			return null;
613		}
614
615		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
616		int dotPosition = filename.lastIndexOf(".");
617
618		if (dotPosition != -1) {
619			String extension = filename.substring(dotPosition + 1);
620			// we want the real file extension, not the crypto one
621			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
622				return extractRelevantExtension(filename.substring(0,dotPosition));
623			} else {
624				return extension;
625			}
626		}
627		return null;
628	}
629
630	public String getMimeType() {
631		if (relativeFilePath != null) {
632			int start = relativeFilePath.lastIndexOf('.') + 1;
633			if (start < relativeFilePath.length()) {
634				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
635			} else {
636				return null;
637			}
638		} else {
639			try {
640				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
641			} catch (MalformedURLException e) {
642				return null;
643			}
644		}
645	}
646
647	public boolean treatAsDownloadable() {
648		if (body.trim().contains(" ")) {
649			return false;
650		}
651		try {
652			final URL url = new URL(body);
653			final String ref = url.getRef();
654			final String protocol = url.getProtocol();
655			final boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
656			return (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
657					|| (("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted));
658
659		} catch (MalformedURLException e) {
660			return false;
661		}
662	}
663
664	public boolean bodyIsHeart() {
665		return body != null && UIHelper.HEARTS.contains(body.trim());
666	}
667
668	public FileParams getFileParams() {
669		FileParams params = getLegacyFileParams();
670		if (params != null) {
671			return params;
672		}
673		params = new FileParams();
674		if (this.transferable != null) {
675			params.size = this.transferable.getFileSize();
676		}
677		if (body == null) {
678			return params;
679		}
680		String parts[] = body.split("\\|");
681		switch (parts.length) {
682			case 1:
683				try {
684					params.size = Long.parseLong(parts[0]);
685				} catch (NumberFormatException e) {
686					try {
687						params.url = new URL(parts[0]);
688					} catch (MalformedURLException e1) {
689						params.url = null;
690					}
691				}
692				break;
693			case 2:
694			case 4:
695				try {
696					params.url = new URL(parts[0]);
697				} catch (MalformedURLException e1) {
698					params.url = null;
699				}
700				try {
701					params.size = Long.parseLong(parts[1]);
702				} catch (NumberFormatException e) {
703					params.size = 0;
704				}
705				try {
706					params.width = Integer.parseInt(parts[2]);
707				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
708					params.width = 0;
709				}
710				try {
711					params.height = Integer.parseInt(parts[3]);
712				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
713					params.height = 0;
714				}
715				break;
716			case 3:
717				try {
718					params.size = Long.parseLong(parts[0]);
719				} catch (NumberFormatException e) {
720					params.size = 0;
721				}
722				try {
723					params.width = Integer.parseInt(parts[1]);
724				} catch (NumberFormatException e) {
725					params.width = 0;
726				}
727				try {
728					params.height = Integer.parseInt(parts[2]);
729				} catch (NumberFormatException e) {
730					params.height = 0;
731				}
732				break;
733		}
734		return params;
735	}
736
737	public FileParams getLegacyFileParams() {
738		FileParams params = new FileParams();
739		if (body == null) {
740			return params;
741		}
742		String parts[] = body.split(",");
743		if (parts.length == 3) {
744			try {
745				params.size = Long.parseLong(parts[0]);
746			} catch (NumberFormatException e) {
747				return null;
748			}
749			try {
750				params.width = Integer.parseInt(parts[1]);
751			} catch (NumberFormatException e) {
752				return null;
753			}
754			try {
755				params.height = Integer.parseInt(parts[2]);
756			} catch (NumberFormatException e) {
757				return null;
758			}
759			return params;
760		} else {
761			return null;
762		}
763	}
764
765	public void untie() {
766		this.mNextMessage = null;
767		this.mPreviousMessage = null;
768	}
769
770	public boolean isFileOrImage() {
771		return type == TYPE_FILE || type == TYPE_IMAGE;
772	}
773
774	public boolean hasFileOnRemoteHost() {
775		return isFileOrImage() && getFileParams().url != null;
776	}
777
778	public boolean needsUploading() {
779		return isFileOrImage() && getFileParams().url == null;
780	}
781
782	public class FileParams {
783		public URL url;
784		public long size = 0;
785		public int width = 0;
786		public int height = 0;
787	}
788
789	public void setFingerprint(String fingerprint) {
790		this.axolotlFingerprint = fingerprint;
791	}
792
793	public String getFingerprint() {
794		return axolotlFingerprint;
795	}
796
797	public boolean isTrusted() {
798		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
799		return s != null && s.isTrusted();
800	}
801
802	private  int getPreviousEncryption() {
803		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
804			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
805				continue;
806			}
807			return iterator.getEncryption();
808		}
809		return ENCRYPTION_NONE;
810	}
811
812	private int getNextEncryption() {
813		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
814			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
815				continue;
816			}
817			return iterator.getEncryption();
818		}
819		return conversation.getNextEncryption();
820	}
821
822	public boolean isValidInSession() {
823		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
824		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
825
826		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
827				|| futureEncryption == ENCRYPTION_NONE
828				|| pastEncryption != futureEncryption;
829
830		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
831	}
832
833	private static int getCleanedEncryption(int encryption) {
834		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
835			return ENCRYPTION_PGP;
836		}
837		return encryption;
838	}
839}