Message.java

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