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