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	private final Conversation conversation;
 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(Conversation conversation) {
 91		this.conversation = conversation;
 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(conversation, 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	}
118
119	private Message(final Conversation conversation, 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.conversation = conversation;
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, Conversation conversation) {
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(conversation,
172				cursor.getString(cursor.getColumnIndex(UUID)),
173				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
174				jid,
175				trueCounterpart,
176				cursor.getString(cursor.getColumnIndex(BODY)),
177				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
178				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
179				cursor.getInt(cursor.getColumnIndex(STATUS)),
180				cursor.getInt(cursor.getColumnIndex(TYPE)),
181				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
182				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
183				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
184				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
185				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
186				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
187				cursor.getString(cursor.getColumnIndex(EDITED)),
188				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
189				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
190	}
191
192	public static Message createStatusMessage(Conversation conversation, String body) {
193		final Message message = new Message(conversation);
194		message.setType(Message.TYPE_STATUS);
195		message.setBody(body);
196		return message;
197	}
198
199	public static Message createLoadMoreMessage(Conversation conversation) {
200		final Message message = new Message(conversation);
201		message.setType(Message.TYPE_STATUS);
202		message.setBody("LOAD_MORE");
203		return message;
204	}
205
206	@Override
207	public ContentValues getContentValues() {
208		ContentValues values = new ContentValues();
209		values.put(UUID, uuid);
210		values.put(CONVERSATION, conversationUuid);
211		if (counterpart == null) {
212			values.putNull(COUNTERPART);
213		} else {
214			values.put(COUNTERPART, counterpart.toPreppedString());
215		}
216		if (trueCounterpart == null) {
217			values.putNull(TRUE_COUNTERPART);
218		} else {
219			values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
220		}
221		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : body);
222		values.put(TIME_SENT, timeSent);
223		values.put(ENCRYPTION, encryption);
224		values.put(STATUS, status);
225		values.put(TYPE, type);
226		values.put(CARBON, carbon ? 1 : 0);
227		values.put(REMOTE_MSG_ID, remoteMsgId);
228		values.put(RELATIVE_FILE_PATH, relativeFilePath);
229		values.put(SERVER_MSG_ID, serverMsgId);
230		values.put(FINGERPRINT, axolotlFingerprint);
231		values.put(READ,read ? 1 : 0);
232		values.put(EDITED, edited);
233		values.put(OOB, oob ? 1 : 0);
234		values.put(ERROR_MESSAGE,errorMessage);
235		return values;
236	}
237
238	public String getConversationUuid() {
239		return conversationUuid;
240	}
241
242	public Conversation getConversation() {
243		return this.conversation;
244	}
245
246	public Jid getCounterpart() {
247		return counterpart;
248	}
249
250	public void setCounterpart(final Jid counterpart) {
251		this.counterpart = counterpart;
252	}
253
254	public Contact getContact() {
255		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
256			return this.conversation.getContact();
257		} else {
258			if (this.trueCounterpart == null) {
259				return null;
260			} else {
261				return this.conversation.getAccount().getRoster()
262						.getContactFromRoster(this.trueCounterpart);
263			}
264		}
265	}
266
267	public String getBody() {
268		return body;
269	}
270
271	public void setBody(String body) {
272		if (body == null) {
273			throw new Error("You should not set the message body to null");
274		}
275		this.body = body;
276	}
277
278	public String getErrorMessage() {
279		return errorMessage;
280	}
281
282	public boolean setErrorMessage(String message) {
283		boolean changed = (message != null && !message.equals(errorMessage))
284				|| (message == null && errorMessage != null);
285		this.errorMessage = message;
286		return changed;
287	}
288
289	public long getTimeSent() {
290		return timeSent;
291	}
292
293	public int getEncryption() {
294		return encryption;
295	}
296
297	public void setEncryption(int encryption) {
298		this.encryption = encryption;
299	}
300
301	public int getStatus() {
302		return status;
303	}
304
305	public void setStatus(int status) {
306		this.status = status;
307	}
308
309	public String getRelativeFilePath() {
310		return this.relativeFilePath;
311	}
312
313	public void setRelativeFilePath(String path) {
314		this.relativeFilePath = path;
315	}
316
317	public String getRemoteMsgId() {
318		return this.remoteMsgId;
319	}
320
321	public void setRemoteMsgId(String id) {
322		this.remoteMsgId = id;
323	}
324
325	public String getServerMsgId() {
326		return this.serverMsgId;
327	}
328
329	public void setServerMsgId(String id) {
330		this.serverMsgId = id;
331	}
332
333	public boolean isRead() {
334		return this.read;
335	}
336
337	public void markRead() {
338		this.read = true;
339	}
340
341	public void markUnread() {
342		this.read = false;
343	}
344
345	public void setTime(long time) {
346		this.timeSent = time;
347	}
348
349	public String getEncryptedBody() {
350		return this.encryptedBody;
351	}
352
353	public void setEncryptedBody(String body) {
354		this.encryptedBody = body;
355	}
356
357	public int getType() {
358		return this.type;
359	}
360
361	public void setType(int type) {
362		this.type = type;
363	}
364
365	public boolean isCarbon() {
366		return carbon;
367	}
368
369	public void setCarbon(boolean carbon) {
370		this.carbon = carbon;
371	}
372
373	public void setEdited(String edited) {
374		this.edited = edited;
375	}
376
377	public boolean edited() {
378		return this.edited != null;
379	}
380
381	public void setTrueCounterpart(Jid trueCounterpart) {
382		this.trueCounterpart = trueCounterpart;
383	}
384
385	public Jid getTrueCounterpart() {
386		return this.trueCounterpart;
387	}
388
389	public Transferable getTransferable() {
390		return this.transferable;
391	}
392
393	public void setTransferable(Transferable transferable) {
394		this.transferable = transferable;
395	}
396
397	public boolean similar(Message message) {
398		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
399			return this.serverMsgId.equals(message.getServerMsgId());
400		} else if (this.body == null || this.counterpart == null) {
401			return false;
402		} else {
403			String body, otherBody;
404			if (this.hasFileOnRemoteHost()) {
405				body = getFileParams().url.toString();
406				otherBody = message.body == null ? null : message.body.trim();
407			} else {
408				body = this.body;
409				otherBody = message.body;
410			}
411			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
412			if (message.getRemoteMsgId() != null) {
413				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
414				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
415					return true;
416				}
417				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
418						&& matchingCounterpart
419						&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
420			} else {
421				return this.remoteMsgId == null
422						&& matchingCounterpart
423						&& body.equals(otherBody)
424						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
425			}
426		}
427	}
428
429	public Message next() {
430		synchronized (this.conversation.messages) {
431			if (this.mNextMessage == null) {
432				int index = this.conversation.messages.indexOf(this);
433				if (index < 0 || index >= this.conversation.messages.size() - 1) {
434					this.mNextMessage = null;
435				} else {
436					this.mNextMessage = this.conversation.messages.get(index + 1);
437				}
438			}
439			return this.mNextMessage;
440		}
441	}
442
443	public Message prev() {
444		synchronized (this.conversation.messages) {
445			if (this.mPreviousMessage == null) {
446				int index = this.conversation.messages.indexOf(this);
447				if (index <= 0 || index > this.conversation.messages.size()) {
448					this.mPreviousMessage = null;
449				} else {
450					this.mPreviousMessage = this.conversation.messages.get(index - 1);
451				}
452			}
453			return this.mPreviousMessage;
454		}
455	}
456
457	public boolean isLastCorrectableMessage() {
458		Message next = next();
459		while(next != null) {
460			if (next.isCorrectable()) {
461				return false;
462			}
463			next = next.next();
464		}
465		return isCorrectable();
466	}
467
468	private boolean isCorrectable() {
469		return getStatus() != STATUS_RECEIVED && !isCarbon();
470	}
471
472	public boolean mergeable(final Message message) {
473		return message != null &&
474				(message.getType() == Message.TYPE_TEXT &&
475						this.getTransferable() == null &&
476						message.getTransferable() == null &&
477						message.getEncryption() != Message.ENCRYPTION_PGP &&
478						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
479						this.getType() == message.getType() &&
480						//this.getStatus() == message.getStatus() &&
481						isStatusMergeable(this.getStatus(), message.getStatus()) &&
482						this.getEncryption() == message.getEncryption() &&
483						this.getCounterpart() != null &&
484						this.getCounterpart().equals(message.getCounterpart()) &&
485						this.edited() == message.edited() &&
486						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
487						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
488						!GeoHelper.isGeoUri(message.getBody()) &&
489						!GeoHelper.isGeoUri(this.body) &&
490						!message.treatAsDownloadable() &&
491						!this.treatAsDownloadable() &&
492						!message.getBody().startsWith(ME_COMMAND) &&
493						!this.getBody().startsWith(ME_COMMAND) &&
494						!this.bodyIsHeart() &&
495						!message.bodyIsHeart() &&
496						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
497				);
498	}
499
500	private static boolean isStatusMergeable(int a, int b) {
501		return a == b || (
502				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
503						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
504						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
505						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
506						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
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	private static String extractRelevantExtension(URL url) {
601		String path = url.getPath();
602		return extractRelevantExtension(path);
603	}
604
605	private static String extractRelevantExtension(String path) {
606		if (path == null || path.isEmpty()) {
607			return null;
608		}
609
610		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
611		int dotPosition = filename.lastIndexOf(".");
612
613		if (dotPosition != -1) {
614			String extension = filename.substring(dotPosition + 1);
615			// we want the real file extension, not the crypto one
616			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
617				return extractRelevantExtension(filename.substring(0,dotPosition));
618			} else {
619				return extension;
620			}
621		}
622		return null;
623	}
624
625	public String getMimeType() {
626		if (relativeFilePath != null) {
627			int start = relativeFilePath.lastIndexOf('.') + 1;
628			if (start < relativeFilePath.length()) {
629				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
630			} else {
631				return null;
632			}
633		} else {
634			try {
635				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
636			} catch (MalformedURLException e) {
637				return null;
638			}
639		}
640	}
641
642	public boolean treatAsDownloadable() {
643		if (body.trim().contains(" ")) {
644			return false;
645		}
646		try {
647			final URL url = new URL(body);
648			final String ref = url.getRef();
649			final String protocol = url.getProtocol();
650			final boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
651			return (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
652					|| (("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted));
653
654		} catch (MalformedURLException e) {
655			return false;
656		}
657	}
658
659	public boolean bodyIsHeart() {
660		return body != null && UIHelper.HEARTS.contains(body.trim());
661	}
662
663	public FileParams getFileParams() {
664		FileParams params = getLegacyFileParams();
665		if (params != null) {
666			return params;
667		}
668		params = new FileParams();
669		if (this.transferable != null) {
670			params.size = this.transferable.getFileSize();
671		}
672		if (body == null) {
673			return params;
674		}
675		String parts[] = body.split("\\|");
676		switch (parts.length) {
677			case 1:
678				try {
679					params.size = Long.parseLong(parts[0]);
680				} catch (NumberFormatException e) {
681					try {
682						params.url = new URL(parts[0]);
683					} catch (MalformedURLException e1) {
684						params.url = null;
685					}
686				}
687				break;
688			case 2:
689			case 4:
690				try {
691					params.url = new URL(parts[0]);
692				} catch (MalformedURLException e1) {
693					params.url = null;
694				}
695				try {
696					params.size = Long.parseLong(parts[1]);
697				} catch (NumberFormatException e) {
698					params.size = 0;
699				}
700				try {
701					params.width = Integer.parseInt(parts[2]);
702				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
703					params.width = 0;
704				}
705				try {
706					params.height = Integer.parseInt(parts[3]);
707				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
708					params.height = 0;
709				}
710				break;
711			case 3:
712				try {
713					params.size = Long.parseLong(parts[0]);
714				} catch (NumberFormatException e) {
715					params.size = 0;
716				}
717				try {
718					params.width = Integer.parseInt(parts[1]);
719				} catch (NumberFormatException e) {
720					params.width = 0;
721				}
722				try {
723					params.height = Integer.parseInt(parts[2]);
724				} catch (NumberFormatException e) {
725					params.height = 0;
726				}
727				break;
728		}
729		return params;
730	}
731
732	public FileParams getLegacyFileParams() {
733		FileParams params = new FileParams();
734		if (body == null) {
735			return params;
736		}
737		String parts[] = body.split(",");
738		if (parts.length == 3) {
739			try {
740				params.size = Long.parseLong(parts[0]);
741			} catch (NumberFormatException e) {
742				return null;
743			}
744			try {
745				params.width = Integer.parseInt(parts[1]);
746			} catch (NumberFormatException e) {
747				return null;
748			}
749			try {
750				params.height = Integer.parseInt(parts[2]);
751			} catch (NumberFormatException e) {
752				return null;
753			}
754			return params;
755		} else {
756			return null;
757		}
758	}
759
760	public void untie() {
761		this.mNextMessage = null;
762		this.mPreviousMessage = null;
763	}
764
765	public boolean isFileOrImage() {
766		return type == TYPE_FILE || type == TYPE_IMAGE;
767	}
768
769	public boolean hasFileOnRemoteHost() {
770		return isFileOrImage() && getFileParams().url != null;
771	}
772
773	public boolean needsUploading() {
774		return isFileOrImage() && getFileParams().url == null;
775	}
776
777	public class FileParams {
778		public URL url;
779		public long size = 0;
780		public int width = 0;
781		public int height = 0;
782	}
783
784	public void setFingerprint(String fingerprint) {
785		this.axolotlFingerprint = fingerprint;
786	}
787
788	public String getFingerprint() {
789		return axolotlFingerprint;
790	}
791
792	public boolean isTrusted() {
793		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
794		return s != null && s.isTrusted();
795	}
796
797	private  int getPreviousEncryption() {
798		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
799			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
800				continue;
801			}
802			return iterator.getEncryption();
803		}
804		return ENCRYPTION_NONE;
805	}
806
807	private int getNextEncryption() {
808		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
809			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
810				continue;
811			}
812			return iterator.getEncryption();
813		}
814		return conversation.getNextEncryption();
815	}
816
817	public boolean isValidInSession() {
818		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
819		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
820
821		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
822				|| futureEncryption == ENCRYPTION_NONE
823				|| pastEncryption != futureEncryption;
824
825		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
826	}
827
828	private static int getCleanedEncryption(int encryption) {
829		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
830			return ENCRYPTION_PGP;
831		}
832		return encryption;
833	}
834}