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.crypto.axolotl.XmppAxolotlSession;
 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		}
158		Jid trueCounterpart;
159		try {
160			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
161			if (value != null) {
162				trueCounterpart = Jid.fromString(value, true);
163			} else {
164				trueCounterpart = null;
165			}
166		} catch (InvalidJidException e) {
167			trueCounterpart = null;
168		}
169		return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
170				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
171				jid,
172				trueCounterpart,
173				cursor.getString(cursor.getColumnIndex(BODY)),
174				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
175				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
176				cursor.getInt(cursor.getColumnIndex(STATUS)),
177				cursor.getInt(cursor.getColumnIndex(TYPE)),
178				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
179				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
180				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
181				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
182				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
183				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
184				cursor.getString(cursor.getColumnIndex(EDITED)),
185				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
186				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
187	}
188
189	public static Message createStatusMessage(Conversation conversation, String body) {
190		final Message message = new Message();
191		message.setType(Message.TYPE_STATUS);
192		message.setConversation(conversation);
193		message.setBody(body);
194		return message;
195	}
196
197	public static Message createLoadMoreMessage(Conversation conversation) {
198		final Message message = new Message();
199		message.setType(Message.TYPE_STATUS);
200		message.setConversation(conversation);
201		message.setBody("LOAD_MORE");
202		return message;
203	}
204
205	@Override
206	public ContentValues getContentValues() {
207		ContentValues values = new ContentValues();
208		values.put(UUID, uuid);
209		values.put(CONVERSATION, conversationUuid);
210		if (counterpart == null) {
211			values.putNull(COUNTERPART);
212		} else {
213			values.put(COUNTERPART, counterpart.toPreppedString());
214		}
215		if (trueCounterpart == null) {
216			values.putNull(TRUE_COUNTERPART);
217		} else {
218			values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
219		}
220		values.put(BODY, body);
221		values.put(TIME_SENT, timeSent);
222		values.put(ENCRYPTION, encryption);
223		values.put(STATUS, status);
224		values.put(TYPE, type);
225		values.put(CARBON, carbon ? 1 : 0);
226		values.put(REMOTE_MSG_ID, remoteMsgId);
227		values.put(RELATIVE_FILE_PATH, relativeFilePath);
228		values.put(SERVER_MSG_ID, serverMsgId);
229		values.put(FINGERPRINT, axolotlFingerprint);
230		values.put(READ,read ? 1 : 0);
231		values.put(EDITED, edited);
232		values.put(OOB, oob ? 1 : 0);
233		values.put(ERROR_MESSAGE,errorMessage);
234		return values;
235	}
236
237	public String getConversationUuid() {
238		return conversationUuid;
239	}
240
241	public Conversation getConversation() {
242		return this.conversation;
243	}
244
245	public void setConversation(Conversation conv) {
246		this.conversation = conv;
247	}
248
249	public Jid getCounterpart() {
250		return counterpart;
251	}
252
253	public void setCounterpart(final Jid counterpart) {
254		this.counterpart = counterpart;
255	}
256
257	public Contact getContact() {
258		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
259			return this.conversation.getContact();
260		} else {
261			if (this.trueCounterpart == null) {
262				return null;
263			} else {
264				return this.conversation.getAccount().getRoster()
265						.getContactFromRoster(this.trueCounterpart);
266			}
267		}
268	}
269
270	public String getBody() {
271		return body;
272	}
273
274	public void setBody(String body) {
275		if (body == null) {
276			throw new Error("You should not set the message body to null");
277		}
278		this.body = body;
279	}
280
281	public String getErrorMessage() {
282		return errorMessage;
283	}
284
285	public boolean setErrorMessage(String message) {
286		boolean changed = (message != null && !message.equals(errorMessage))
287				|| (message == null && errorMessage != null);
288		this.errorMessage = message;
289		return changed;
290	}
291
292	public long getTimeSent() {
293		return timeSent;
294	}
295
296	public int getEncryption() {
297		return encryption;
298	}
299
300	public void setEncryption(int encryption) {
301		this.encryption = encryption;
302	}
303
304	public int getStatus() {
305		return status;
306	}
307
308	public void setStatus(int status) {
309		this.status = status;
310	}
311
312	public String getRelativeFilePath() {
313		return this.relativeFilePath;
314	}
315
316	public void setRelativeFilePath(String path) {
317		this.relativeFilePath = path;
318	}
319
320	public String getRemoteMsgId() {
321		return this.remoteMsgId;
322	}
323
324	public void setRemoteMsgId(String id) {
325		this.remoteMsgId = id;
326	}
327
328	public String getServerMsgId() {
329		return this.serverMsgId;
330	}
331
332	public void setServerMsgId(String id) {
333		this.serverMsgId = id;
334	}
335
336	public boolean isRead() {
337		return this.read;
338	}
339
340	public void markRead() {
341		this.read = true;
342	}
343
344	public void markUnread() {
345		this.read = false;
346	}
347
348	public void setTime(long time) {
349		this.timeSent = time;
350	}
351
352	public String getEncryptedBody() {
353		return this.encryptedBody;
354	}
355
356	public void setEncryptedBody(String body) {
357		this.encryptedBody = body;
358	}
359
360	public int getType() {
361		return this.type;
362	}
363
364	public void setType(int type) {
365		this.type = type;
366	}
367
368	public boolean isCarbon() {
369		return carbon;
370	}
371
372	public void setCarbon(boolean carbon) {
373		this.carbon = carbon;
374	}
375
376	public void setEdited(String edited) {
377		this.edited = edited;
378	}
379
380	public boolean edited() {
381		return this.edited != null;
382	}
383
384	public void setTrueCounterpart(Jid trueCounterpart) {
385		this.trueCounterpart = trueCounterpart;
386	}
387
388	public Jid getTrueCounterpart() {
389		return this.trueCounterpart;
390	}
391
392	public Transferable getTransferable() {
393		return this.transferable;
394	}
395
396	public void setTransferable(Transferable transferable) {
397		this.transferable = transferable;
398	}
399
400	public boolean similar(Message message) {
401		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
402			return this.serverMsgId.equals(message.getServerMsgId());
403		} else if (this.body == null || this.counterpart == null) {
404			return false;
405		} else {
406			String body, otherBody;
407			if (this.hasFileOnRemoteHost()) {
408				body = getFileParams().url.toString();
409				otherBody = message.body == null ? null : message.body.trim();
410			} else {
411				body = this.body;
412				otherBody = message.body;
413			}
414			if (message.getRemoteMsgId() != null) {
415				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
416						&& this.counterpart.equals(message.getCounterpart())
417						&& (body.equals(otherBody)
418						||(message.getEncryption() == Message.ENCRYPTION_PGP
419						&& CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches()));
420			} else {
421				return this.remoteMsgId == null
422						&& this.counterpart.equals(message.getCounterpart())
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() == Decision.NEVER &&
491						this.treatAsDownloadable() == Decision.NEVER &&
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_UNSEND && b == Message.STATUS_SEND)
505						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
506						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
507						|| (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
508		);
509	}
510
511	public static class MergeSeparator {}
512
513	public SpannableStringBuilder getMergedBody() {
514		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
515		Message current = this;
516		while (current.mergeable(current.next())) {
517			current = current.next();
518			if (current == null) {
519				break;
520			}
521			body.append("\n\n");
522			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
523					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
524			body.append(current.getBody().trim());
525		}
526		return body;
527	}
528
529	public boolean hasMeCommand() {
530		return this.body.trim().startsWith(ME_COMMAND);
531	}
532
533	public int getMergedStatus() {
534		int status = this.status;
535		Message current = this;
536		while(current.mergeable(current.next())) {
537			current = current.next();
538			if (current == null) {
539				break;
540			}
541			status = current.status;
542		}
543		return status;
544	}
545
546	public long getMergedTimeSent() {
547		long time = this.timeSent;
548		Message current = this;
549		while(current.mergeable(current.next())) {
550			current = current.next();
551			if (current == null) {
552				break;
553			}
554			time = current.timeSent;
555		}
556		return time;
557	}
558
559	public boolean wasMergedIntoPrevious() {
560		Message prev = this.prev();
561		return prev != null && prev.mergeable(this);
562	}
563
564	public boolean trusted() {
565		Contact contact = this.getContact();
566		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
567	}
568
569	public boolean fixCounterpart() {
570		Presences presences = conversation.getContact().getPresences();
571		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
572			return true;
573		} else if (presences.size() >= 1) {
574			try {
575				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
576						conversation.getJid().getDomainpart(),
577						presences.toResourceArray()[0]);
578				return true;
579			} catch (InvalidJidException e) {
580				counterpart = null;
581				return false;
582			}
583		} else {
584			counterpart = null;
585			return false;
586		}
587	}
588
589	public void setUuid(String uuid) {
590		this.uuid = uuid;
591	}
592
593	public String getEditedId() {
594		return edited;
595	}
596
597	public void setOob(boolean isOob) {
598		this.oob = isOob;
599	}
600
601	public enum Decision {
602		MUST,
603		SHOULD,
604		NEVER,
605	}
606
607	private static String extractRelevantExtension(URL url) {
608		String path = url.getPath();
609		return extractRelevantExtension(path);
610	}
611
612	private static String extractRelevantExtension(String path) {
613		if (path == null || path.isEmpty()) {
614			return null;
615		}
616
617		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
618		int dotPosition = filename.lastIndexOf(".");
619
620		if (dotPosition != -1) {
621			String extension = filename.substring(dotPosition + 1);
622			// we want the real file extension, not the crypto one
623			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
624				return extractRelevantExtension(filename.substring(0,dotPosition));
625			} else {
626				return extension;
627			}
628		}
629		return null;
630	}
631
632	public String getMimeType() {
633		if (relativeFilePath != null) {
634			int start = relativeFilePath.lastIndexOf('.') + 1;
635			if (start < relativeFilePath.length()) {
636				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
637			} else {
638				return null;
639			}
640		} else {
641			try {
642				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
643			} catch (MalformedURLException e) {
644				return null;
645			}
646		}
647	}
648
649	public Decision treatAsDownloadable() {
650		if (body.trim().contains(" ")) {
651			return Decision.NEVER;
652		}
653		try {
654			URL url = new URL(body);
655			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
656				return Decision.NEVER;
657			} else if (oob) {
658				return Decision.MUST;
659			}
660			String extension = extractRelevantExtension(url);
661			if (extension == null) {
662				return Decision.NEVER;
663			}
664			String ref = url.getRef();
665			boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
666
667			if (encrypted) {
668				return Decision.MUST;
669			} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
670					|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
671				return Decision.SHOULD;
672			} else {
673				return Decision.NEVER;
674			}
675
676		} catch (MalformedURLException e) {
677			return Decision.NEVER;
678		}
679	}
680
681	public boolean bodyIsHeart() {
682		return body != null && UIHelper.HEARTS.contains(body.trim());
683	}
684
685	public FileParams getFileParams() {
686		FileParams params = getLegacyFileParams();
687		if (params != null) {
688			return params;
689		}
690		params = new FileParams();
691		if (this.transferable != null) {
692			params.size = this.transferable.getFileSize();
693		}
694		if (body == null) {
695			return params;
696		}
697		String parts[] = body.split("\\|");
698		switch (parts.length) {
699			case 1:
700				try {
701					params.size = Long.parseLong(parts[0]);
702				} catch (NumberFormatException e) {
703					try {
704						params.url = new URL(parts[0]);
705					} catch (MalformedURLException e1) {
706						params.url = null;
707					}
708				}
709				break;
710			case 2:
711			case 4:
712				try {
713					params.url = new URL(parts[0]);
714				} catch (MalformedURLException e1) {
715					params.url = null;
716				}
717				try {
718					params.size = Long.parseLong(parts[1]);
719				} catch (NumberFormatException e) {
720					params.size = 0;
721				}
722				try {
723					params.width = Integer.parseInt(parts[2]);
724				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
725					params.width = 0;
726				}
727				try {
728					params.height = Integer.parseInt(parts[3]);
729				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
730					params.height = 0;
731				}
732				break;
733			case 3:
734				try {
735					params.size = Long.parseLong(parts[0]);
736				} catch (NumberFormatException e) {
737					params.size = 0;
738				}
739				try {
740					params.width = Integer.parseInt(parts[1]);
741				} catch (NumberFormatException e) {
742					params.width = 0;
743				}
744				try {
745					params.height = Integer.parseInt(parts[2]);
746				} catch (NumberFormatException e) {
747					params.height = 0;
748				}
749				break;
750		}
751		return params;
752	}
753
754	public FileParams getLegacyFileParams() {
755		FileParams params = new FileParams();
756		if (body == null) {
757			return params;
758		}
759		String parts[] = body.split(",");
760		if (parts.length == 3) {
761			try {
762				params.size = Long.parseLong(parts[0]);
763			} catch (NumberFormatException e) {
764				return null;
765			}
766			try {
767				params.width = Integer.parseInt(parts[1]);
768			} catch (NumberFormatException e) {
769				return null;
770			}
771			try {
772				params.height = Integer.parseInt(parts[2]);
773			} catch (NumberFormatException e) {
774				return null;
775			}
776			return params;
777		} else {
778			return null;
779		}
780	}
781
782	public void untie() {
783		this.mNextMessage = null;
784		this.mPreviousMessage = null;
785	}
786
787	public boolean isFileOrImage() {
788		return type == TYPE_FILE || type == TYPE_IMAGE;
789	}
790
791	public boolean hasFileOnRemoteHost() {
792		return isFileOrImage() && getFileParams().url != null;
793	}
794
795	public boolean needsUploading() {
796		return isFileOrImage() && getFileParams().url == null;
797	}
798
799	public class FileParams {
800		public URL url;
801		public long size = 0;
802		public int width = 0;
803		public int height = 0;
804	}
805
806	public void setFingerprint(String fingerprint) {
807		this.axolotlFingerprint = fingerprint;
808	}
809
810	public String getFingerprint() {
811		return axolotlFingerprint;
812	}
813
814	public boolean isTrusted() {
815		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
816		return s != null && s.isTrusted();
817	}
818
819	private  int getPreviousEncryption() {
820		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
821			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
822				continue;
823			}
824			return iterator.getEncryption();
825		}
826		return ENCRYPTION_NONE;
827	}
828
829	private int getNextEncryption() {
830		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
831			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
832				continue;
833			}
834			return iterator.getEncryption();
835		}
836		return conversation.getNextEncryption();
837	}
838
839	public boolean isValidInSession() {
840		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
841		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
842
843		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
844				|| futureEncryption == ENCRYPTION_NONE
845				|| pastEncryption != futureEncryption;
846
847		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
848	}
849
850	private static int getCleanedEncryption(int encryption) {
851		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
852			return ENCRYPTION_PGP;
853		}
854		return encryption;
855	}
856}