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