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			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
415			if (message.getRemoteMsgId() != null) {
416				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
417				if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
418					return true;
419				}
420				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
421						&& matchingCounterpart
422						&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
423			} else {
424				return this.remoteMsgId == null
425						&& matchingCounterpart
426						&& body.equals(otherBody)
427						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
428			}
429		}
430	}
431
432	public Message next() {
433		synchronized (this.conversation.messages) {
434			if (this.mNextMessage == null) {
435				int index = this.conversation.messages.indexOf(this);
436				if (index < 0 || index >= this.conversation.messages.size() - 1) {
437					this.mNextMessage = null;
438				} else {
439					this.mNextMessage = this.conversation.messages.get(index + 1);
440				}
441			}
442			return this.mNextMessage;
443		}
444	}
445
446	public Message prev() {
447		synchronized (this.conversation.messages) {
448			if (this.mPreviousMessage == null) {
449				int index = this.conversation.messages.indexOf(this);
450				if (index <= 0 || index > this.conversation.messages.size()) {
451					this.mPreviousMessage = null;
452				} else {
453					this.mPreviousMessage = this.conversation.messages.get(index - 1);
454				}
455			}
456			return this.mPreviousMessage;
457		}
458	}
459
460	public boolean isLastCorrectableMessage() {
461		Message next = next();
462		while(next != null) {
463			if (next.isCorrectable()) {
464				return false;
465			}
466			next = next.next();
467		}
468		return isCorrectable();
469	}
470
471	private boolean isCorrectable() {
472		return getStatus() != STATUS_RECEIVED && !isCarbon();
473	}
474
475	public boolean mergeable(final Message message) {
476		return message != null &&
477				(message.getType() == Message.TYPE_TEXT &&
478						this.getTransferable() == null &&
479						message.getTransferable() == null &&
480						message.getEncryption() != Message.ENCRYPTION_PGP &&
481						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
482						this.getType() == message.getType() &&
483						//this.getStatus() == message.getStatus() &&
484						isStatusMergeable(this.getStatus(), message.getStatus()) &&
485						this.getEncryption() == message.getEncryption() &&
486						this.getCounterpart() != null &&
487						this.getCounterpart().equals(message.getCounterpart()) &&
488						this.edited() == message.edited() &&
489						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
490						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
491						!GeoHelper.isGeoUri(message.getBody()) &&
492						!GeoHelper.isGeoUri(this.body) &&
493						message.treatAsDownloadable() == Decision.NEVER &&
494						this.treatAsDownloadable() == Decision.NEVER &&
495						!message.getBody().startsWith(ME_COMMAND) &&
496						!this.getBody().startsWith(ME_COMMAND) &&
497						!this.bodyIsHeart() &&
498						!message.bodyIsHeart() &&
499						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
500				);
501	}
502
503	private static boolean isStatusMergeable(int a, int b) {
504		return a == b || (
505				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
506						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
507						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND)
508						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
509						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
510						|| (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
511		);
512	}
513
514	public static class MergeSeparator {}
515
516	public SpannableStringBuilder getMergedBody() {
517		SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
518		Message current = this;
519		while (current.mergeable(current.next())) {
520			current = current.next();
521			if (current == null) {
522				break;
523			}
524			body.append("\n\n");
525			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
526					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
527			body.append(current.getBody().trim());
528		}
529		return body;
530	}
531
532	public boolean hasMeCommand() {
533		return this.body.trim().startsWith(ME_COMMAND);
534	}
535
536	public int getMergedStatus() {
537		int status = this.status;
538		Message current = this;
539		while(current.mergeable(current.next())) {
540			current = current.next();
541			if (current == null) {
542				break;
543			}
544			status = current.status;
545		}
546		return status;
547	}
548
549	public long getMergedTimeSent() {
550		long time = this.timeSent;
551		Message current = this;
552		while(current.mergeable(current.next())) {
553			current = current.next();
554			if (current == null) {
555				break;
556			}
557			time = current.timeSent;
558		}
559		return time;
560	}
561
562	public boolean wasMergedIntoPrevious() {
563		Message prev = this.prev();
564		return prev != null && prev.mergeable(this);
565	}
566
567	public boolean trusted() {
568		Contact contact = this.getContact();
569		return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
570	}
571
572	public boolean fixCounterpart() {
573		Presences presences = conversation.getContact().getPresences();
574		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
575			return true;
576		} else if (presences.size() >= 1) {
577			try {
578				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
579						conversation.getJid().getDomainpart(),
580						presences.toResourceArray()[0]);
581				return true;
582			} catch (InvalidJidException e) {
583				counterpart = null;
584				return false;
585			}
586		} else {
587			counterpart = null;
588			return false;
589		}
590	}
591
592	public void setUuid(String uuid) {
593		this.uuid = uuid;
594	}
595
596	public String getEditedId() {
597		return edited;
598	}
599
600	public void setOob(boolean isOob) {
601		this.oob = isOob;
602	}
603
604	public enum Decision {
605		MUST,
606		SHOULD,
607		NEVER,
608	}
609
610	private static String extractRelevantExtension(URL url) {
611		String path = url.getPath();
612		return extractRelevantExtension(path);
613	}
614
615	private static String extractRelevantExtension(String path) {
616		if (path == null || path.isEmpty()) {
617			return null;
618		}
619
620		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
621		int dotPosition = filename.lastIndexOf(".");
622
623		if (dotPosition != -1) {
624			String extension = filename.substring(dotPosition + 1);
625			// we want the real file extension, not the crypto one
626			if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
627				return extractRelevantExtension(filename.substring(0,dotPosition));
628			} else {
629				return extension;
630			}
631		}
632		return null;
633	}
634
635	public String getMimeType() {
636		if (relativeFilePath != null) {
637			int start = relativeFilePath.lastIndexOf('.') + 1;
638			if (start < relativeFilePath.length()) {
639				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
640			} else {
641				return null;
642			}
643		} else {
644			try {
645				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
646			} catch (MalformedURLException e) {
647				return null;
648			}
649		}
650	}
651
652	public Decision treatAsDownloadable() {
653		if (body.trim().contains(" ")) {
654			return Decision.NEVER;
655		}
656		try {
657			URL url = new URL(body);
658			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
659				return Decision.NEVER;
660			} else if (oob) {
661				return Decision.MUST;
662			}
663			String extension = extractRelevantExtension(url);
664			if (extension == null) {
665				return Decision.NEVER;
666			}
667			String ref = url.getRef();
668			boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
669
670			if (encrypted) {
671				return Decision.MUST;
672			} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
673					|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
674				return Decision.SHOULD;
675			} else {
676				return Decision.NEVER;
677			}
678
679		} catch (MalformedURLException e) {
680			return Decision.NEVER;
681		}
682	}
683
684	public boolean bodyIsHeart() {
685		return body != null && UIHelper.HEARTS.contains(body.trim());
686	}
687
688	public FileParams getFileParams() {
689		FileParams params = getLegacyFileParams();
690		if (params != null) {
691			return params;
692		}
693		params = new FileParams();
694		if (this.transferable != null) {
695			params.size = this.transferable.getFileSize();
696		}
697		if (body == null) {
698			return params;
699		}
700		String parts[] = body.split("\\|");
701		switch (parts.length) {
702			case 1:
703				try {
704					params.size = Long.parseLong(parts[0]);
705				} catch (NumberFormatException e) {
706					try {
707						params.url = new URL(parts[0]);
708					} catch (MalformedURLException e1) {
709						params.url = null;
710					}
711				}
712				break;
713			case 2:
714			case 4:
715				try {
716					params.url = new URL(parts[0]);
717				} catch (MalformedURLException e1) {
718					params.url = null;
719				}
720				try {
721					params.size = Long.parseLong(parts[1]);
722				} catch (NumberFormatException e) {
723					params.size = 0;
724				}
725				try {
726					params.width = Integer.parseInt(parts[2]);
727				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
728					params.width = 0;
729				}
730				try {
731					params.height = Integer.parseInt(parts[3]);
732				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
733					params.height = 0;
734				}
735				break;
736			case 3:
737				try {
738					params.size = Long.parseLong(parts[0]);
739				} catch (NumberFormatException e) {
740					params.size = 0;
741				}
742				try {
743					params.width = Integer.parseInt(parts[1]);
744				} catch (NumberFormatException e) {
745					params.width = 0;
746				}
747				try {
748					params.height = Integer.parseInt(parts[2]);
749				} catch (NumberFormatException e) {
750					params.height = 0;
751				}
752				break;
753		}
754		return params;
755	}
756
757	public FileParams getLegacyFileParams() {
758		FileParams params = new FileParams();
759		if (body == null) {
760			return params;
761		}
762		String parts[] = body.split(",");
763		if (parts.length == 3) {
764			try {
765				params.size = Long.parseLong(parts[0]);
766			} catch (NumberFormatException e) {
767				return null;
768			}
769			try {
770				params.width = Integer.parseInt(parts[1]);
771			} catch (NumberFormatException e) {
772				return null;
773			}
774			try {
775				params.height = Integer.parseInt(parts[2]);
776			} catch (NumberFormatException e) {
777				return null;
778			}
779			return params;
780		} else {
781			return null;
782		}
783	}
784
785	public void untie() {
786		this.mNextMessage = null;
787		this.mPreviousMessage = null;
788	}
789
790	public boolean isFileOrImage() {
791		return type == TYPE_FILE || type == TYPE_IMAGE;
792	}
793
794	public boolean hasFileOnRemoteHost() {
795		return isFileOrImage() && getFileParams().url != null;
796	}
797
798	public boolean needsUploading() {
799		return isFileOrImage() && getFileParams().url == null;
800	}
801
802	public class FileParams {
803		public URL url;
804		public long size = 0;
805		public int width = 0;
806		public int height = 0;
807	}
808
809	public void setFingerprint(String fingerprint) {
810		this.axolotlFingerprint = fingerprint;
811	}
812
813	public String getFingerprint() {
814		return axolotlFingerprint;
815	}
816
817	public boolean isTrusted() {
818		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
819		return s != null && s.isTrusted();
820	}
821
822	private  int getPreviousEncryption() {
823		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
824			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
825				continue;
826			}
827			return iterator.getEncryption();
828		}
829		return ENCRYPTION_NONE;
830	}
831
832	private int getNextEncryption() {
833		for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
834			if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
835				continue;
836			}
837			return iterator.getEncryption();
838		}
839		return conversation.getNextEncryption();
840	}
841
842	public boolean isValidInSession() {
843		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
844		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
845
846		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
847				|| futureEncryption == ENCRYPTION_NONE
848				|| pastEncryption != futureEncryption;
849
850		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
851	}
852
853	private static int getCleanedEncryption(int encryption) {
854		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
855			return ENCRYPTION_PGP;
856		}
857		return encryption;
858	}
859}