Message.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5
  6import java.net.MalformedURLException;
  7import java.net.URL;
  8import java.util.Arrays;
  9
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 12import eu.siacs.conversations.utils.GeoHelper;
 13import eu.siacs.conversations.utils.MimeUtils;
 14import eu.siacs.conversations.utils.UIHelper;
 15import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 16import eu.siacs.conversations.xmpp.jid.Jid;
 17
 18public class Message extends AbstractEntity {
 19
 20	public static final String TABLENAME = "messages";
 21
 22	public static final String MERGE_SEPARATOR = " \u200B\n\n";
 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 REMOTE_MSG_ID = "remoteMsgId";
 55	public static final String SERVER_MSG_ID = "serverMsgId";
 56	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 57	public static final String ME_COMMAND = "/me ";
 58
 59
 60	public boolean markable = false;
 61	protected String conversationUuid;
 62	protected Jid counterpart;
 63	protected Jid trueCounterpart;
 64	protected String body;
 65	protected String encryptedBody;
 66	protected long timeSent;
 67	protected int encryption;
 68	protected int status;
 69	protected int type;
 70	private AxolotlService.XmppAxolotlSession axolotlSession = null;
 71	protected String relativeFilePath;
 72	protected boolean read = true;
 73	protected String remoteMsgId = null;
 74	protected String serverMsgId = null;
 75	protected Conversation conversation = null;
 76	protected Transferable transferable = null;
 77	private Message mNextMessage = null;
 78	private Message mPreviousMessage = null;
 79
 80	private Message() {
 81
 82	}
 83
 84	public Message(Conversation conversation, String body, int encryption) {
 85		this(conversation, body, encryption, STATUS_UNSEND);
 86	}
 87
 88	public Message(Conversation conversation, String body, int encryption, int status) {
 89		this(java.util.UUID.randomUUID().toString(),
 90				conversation.getUuid(),
 91				conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
 92				null,
 93				body,
 94				System.currentTimeMillis(),
 95				encryption,
 96				status,
 97				TYPE_TEXT,
 98				null,
 99				null,
100				null);
101		this.conversation = conversation;
102	}
103
104	private Message(final String uuid, final String conversationUUid, final Jid counterpart,
105					final Jid trueCounterpart, final String body, final long timeSent,
106					final int encryption, final int status, final int type, final String remoteMsgId,
107					final String relativeFilePath, final String serverMsgId) {
108		this.uuid = uuid;
109		this.conversationUuid = conversationUUid;
110		this.counterpart = counterpart;
111		this.trueCounterpart = trueCounterpart;
112		this.body = body;
113		this.timeSent = timeSent;
114		this.encryption = encryption;
115		this.status = status;
116		this.type = type;
117		this.remoteMsgId = remoteMsgId;
118		this.relativeFilePath = relativeFilePath;
119		this.serverMsgId = serverMsgId;
120	}
121
122	public static Message fromCursor(Cursor cursor) {
123		Jid jid;
124		try {
125			String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
126			if (value != null) {
127				jid = Jid.fromString(value, true);
128			} else {
129				jid = null;
130			}
131		} catch (InvalidJidException e) {
132			jid = null;
133		}
134		Jid trueCounterpart;
135		try {
136			String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
137			if (value != null) {
138				trueCounterpart = Jid.fromString(value, true);
139			} else {
140				trueCounterpart = null;
141			}
142		} catch (InvalidJidException e) {
143			trueCounterpart = null;
144		}
145		return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
146				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
147				jid,
148				trueCounterpart,
149				cursor.getString(cursor.getColumnIndex(BODY)),
150				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
151				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
152				cursor.getInt(cursor.getColumnIndex(STATUS)),
153				cursor.getInt(cursor.getColumnIndex(TYPE)),
154				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
155				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
156				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
157	}
158
159	public static Message createStatusMessage(Conversation conversation, String body) {
160		Message message = new Message();
161		message.setType(Message.TYPE_STATUS);
162		message.setConversation(conversation);
163		message.setBody(body);
164		return message;
165	}
166
167	@Override
168	public ContentValues getContentValues() {
169		ContentValues values = new ContentValues();
170		values.put(UUID, uuid);
171		values.put(CONVERSATION, conversationUuid);
172		if (counterpart == null) {
173			values.putNull(COUNTERPART);
174		} else {
175			values.put(COUNTERPART, counterpart.toString());
176		}
177		if (trueCounterpart == null) {
178			values.putNull(TRUE_COUNTERPART);
179		} else {
180			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
181		}
182		values.put(BODY, body);
183		values.put(TIME_SENT, timeSent);
184		values.put(ENCRYPTION, encryption);
185		values.put(STATUS, status);
186		values.put(TYPE, type);
187		values.put(REMOTE_MSG_ID, remoteMsgId);
188		values.put(RELATIVE_FILE_PATH, relativeFilePath);
189		values.put(SERVER_MSG_ID, serverMsgId);
190		return values;
191	}
192
193	public String getConversationUuid() {
194		return conversationUuid;
195	}
196
197	public Conversation getConversation() {
198		return this.conversation;
199	}
200
201	public void setConversation(Conversation conv) {
202		this.conversation = conv;
203	}
204
205	public Jid getCounterpart() {
206		return counterpart;
207	}
208
209	public void setCounterpart(final Jid counterpart) {
210		this.counterpart = counterpart;
211	}
212
213	public Contact getContact() {
214		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
215			return this.conversation.getContact();
216		} else {
217			if (this.trueCounterpart == null) {
218				return null;
219			} else {
220				return this.conversation.getAccount().getRoster()
221						.getContactFromRoster(this.trueCounterpart);
222			}
223		}
224	}
225
226	public String getBody() {
227		return body;
228	}
229
230	public void setBody(String body) {
231		this.body = body;
232	}
233
234	public long getTimeSent() {
235		return timeSent;
236	}
237
238	public int getEncryption() {
239		return encryption;
240	}
241
242	public void setEncryption(int encryption) {
243		this.encryption = encryption;
244	}
245
246	public int getStatus() {
247		return status;
248	}
249
250	public void setStatus(int status) {
251		this.status = status;
252	}
253
254	public String getRelativeFilePath() {
255		return this.relativeFilePath;
256	}
257
258	public void setRelativeFilePath(String path) {
259		this.relativeFilePath = path;
260	}
261
262	public String getRemoteMsgId() {
263		return this.remoteMsgId;
264	}
265
266	public void setRemoteMsgId(String id) {
267		this.remoteMsgId = id;
268	}
269
270	public String getServerMsgId() {
271		return this.serverMsgId;
272	}
273
274	public void setServerMsgId(String id) {
275		this.serverMsgId = id;
276	}
277
278	public boolean isRead() {
279		return this.read;
280	}
281
282	public void markRead() {
283		this.read = true;
284	}
285
286	public void markUnread() {
287		this.read = false;
288	}
289
290	public void setTime(long time) {
291		this.timeSent = time;
292	}
293
294	public String getEncryptedBody() {
295		return this.encryptedBody;
296	}
297
298	public void setEncryptedBody(String body) {
299		this.encryptedBody = body;
300	}
301
302	public int getType() {
303		return this.type;
304	}
305
306	public void setType(int type) {
307		this.type = type;
308	}
309
310	public void setTrueCounterpart(Jid trueCounterpart) {
311		this.trueCounterpart = trueCounterpart;
312	}
313
314	public Transferable getTransferable() {
315		return this.transferable;
316	}
317
318	public void setTransferable(Transferable transferable) {
319		this.transferable = transferable;
320	}
321
322	public boolean equals(Message message) {
323		if (this.serverMsgId != null && message.getServerMsgId() != null) {
324			return this.serverMsgId.equals(message.getServerMsgId());
325		} else if (this.body == null || this.counterpart == null) {
326			return false;
327		} else {
328			String body, otherBody;
329			if (this.hasFileOnRemoteHost()) {
330				body = getFileParams().url.toString();
331				otherBody = message.body == null ? null : message.body.trim();
332			} else {
333				body = this.body;
334				otherBody = message.body;
335			}
336			if (message.getRemoteMsgId() != null) {
337				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
338						&& this.counterpart.equals(message.getCounterpart())
339						&& body.equals(otherBody);
340			} else {
341				return this.remoteMsgId == null
342						&& this.counterpart.equals(message.getCounterpart())
343						&& body.equals(otherBody)
344						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
345			}
346		}
347	}
348
349	public Message next() {
350		synchronized (this.conversation.messages) {
351			if (this.mNextMessage == null) {
352				int index = this.conversation.messages.indexOf(this);
353				if (index < 0 || index >= this.conversation.messages.size() - 1) {
354					this.mNextMessage = null;
355				} else {
356					this.mNextMessage = this.conversation.messages.get(index + 1);
357				}
358			}
359			return this.mNextMessage;
360		}
361	}
362
363	public Message prev() {
364		synchronized (this.conversation.messages) {
365			if (this.mPreviousMessage == null) {
366				int index = this.conversation.messages.indexOf(this);
367				if (index <= 0 || index > this.conversation.messages.size()) {
368					this.mPreviousMessage = null;
369				} else {
370					this.mPreviousMessage = this.conversation.messages.get(index - 1);
371				}
372			}
373			return this.mPreviousMessage;
374		}
375	}
376
377	public boolean mergeable(final Message message) {
378		return message != null &&
379				(message.getType() == Message.TYPE_TEXT &&
380						this.getTransferable() == null &&
381						message.getTransferable() == null &&
382						message.getEncryption() != Message.ENCRYPTION_PGP &&
383						this.getType() == message.getType() &&
384						//this.getStatus() == message.getStatus() &&
385						isStatusMergeable(this.getStatus(), message.getStatus()) &&
386						this.getEncryption() == message.getEncryption() &&
387						this.getCounterpart() != null &&
388						this.getCounterpart().equals(message.getCounterpart()) &&
389						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
390						!GeoHelper.isGeoUri(message.getBody()) &&
391						!GeoHelper.isGeoUri(this.body) &&
392						message.treatAsDownloadable() == Decision.NEVER &&
393						this.treatAsDownloadable() == Decision.NEVER &&
394						!message.getBody().startsWith(ME_COMMAND) &&
395						!this.getBody().startsWith(ME_COMMAND) &&
396						!this.bodyIsHeart() &&
397						!message.bodyIsHeart()
398				);
399	}
400
401	private static boolean isStatusMergeable(int a, int b) {
402		return a == b || (
403				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
404						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
405						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND)
406						|| (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
407						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
408						|| (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
409		);
410	}
411
412	public String getMergedBody() {
413		final Message next = this.next();
414		if (this.mergeable(next)) {
415			return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody();
416		}
417		return getBody().trim();
418	}
419
420	public boolean hasMeCommand() {
421		return getMergedBody().startsWith(ME_COMMAND);
422	}
423
424	public int getMergedStatus() {
425		final Message next = this.next();
426		if (this.mergeable(next)) {
427			return next.getStatus();
428		}
429		return getStatus();
430	}
431
432	public long getMergedTimeSent() {
433		Message next = this.next();
434		if (this.mergeable(next)) {
435			return next.getMergedTimeSent();
436		} else {
437			return getTimeSent();
438		}
439	}
440
441	public boolean wasMergedIntoPrevious() {
442		Message prev = this.prev();
443		return prev != null && prev.mergeable(this);
444	}
445
446	public boolean trusted() {
447		Contact contact = this.getContact();
448		return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
449	}
450
451	public boolean fixCounterpart() {
452		Presences presences = conversation.getContact().getPresences();
453		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
454			return true;
455		} else if (presences.size() >= 1) {
456			try {
457				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
458						conversation.getJid().getDomainpart(),
459						presences.asStringArray()[0]);
460				return true;
461			} catch (InvalidJidException e) {
462				counterpart = null;
463				return false;
464			}
465		} else {
466			counterpart = null;
467			return false;
468		}
469	}
470
471	public enum Decision {
472		MUST,
473		SHOULD,
474		NEVER,
475	}
476
477	private static String extractRelevantExtension(URL url) {
478		String path = url.getPath();
479		if (path == null || path.isEmpty()) {
480			return null;
481		}
482		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
483		String[] extensionParts = filename.split("\\.");
484		if (extensionParts.length == 2) {
485			return extensionParts[extensionParts.length - 1];
486		} else if (extensionParts.length == 3 && Arrays
487				.asList(Transferable.VALID_CRYPTO_EXTENSIONS)
488				.contains(extensionParts[extensionParts.length - 1])) {
489			return extensionParts[extensionParts.length -2];
490		}
491		return null;
492	}
493
494	public String getMimeType() {
495		if (relativeFilePath != null) {
496			int start = relativeFilePath.lastIndexOf('.') + 1;
497			if (start < relativeFilePath.length()) {
498				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
499			} else {
500				return null;
501			}
502		} else {
503			try {
504				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
505			} catch (MalformedURLException e) {
506				return null;
507			}
508		}
509	}
510
511	public Decision treatAsDownloadable() {
512		if (body.trim().contains(" ")) {
513			return Decision.NEVER;
514		}
515		try {
516			URL url = new URL(body);
517			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
518				return Decision.NEVER;
519			}
520			String extension = extractRelevantExtension(url);
521			if (extension == null) {
522				return Decision.NEVER;
523			}
524			String ref = url.getRef();
525			boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
526
527			if (encrypted) {
528				if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
529					return Decision.MUST;
530				} else {
531					return Decision.NEVER;
532				}
533			} else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)
534					|| Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
535				return Decision.SHOULD;
536			} else {
537				return Decision.NEVER;
538			}
539
540		} catch (MalformedURLException e) {
541			return Decision.NEVER;
542		}
543	}
544
545	public boolean bodyIsHeart() {
546		return body != null && UIHelper.HEARTS.contains(body.trim());
547	}
548
549	public FileParams getFileParams() {
550		FileParams params = getLegacyFileParams();
551		if (params != null) {
552			return params;
553		}
554		params = new FileParams();
555		if (this.transferable != null) {
556			params.size = this.transferable.getFileSize();
557		}
558		if (body == null) {
559			return params;
560		}
561		String parts[] = body.split("\\|");
562		switch (parts.length) {
563			case 1:
564				try {
565					params.size = Long.parseLong(parts[0]);
566				} catch (NumberFormatException e) {
567					try {
568						params.url = new URL(parts[0]);
569					} catch (MalformedURLException e1) {
570						params.url = null;
571					}
572				}
573				break;
574			case 2:
575			case 4:
576				try {
577					params.url = new URL(parts[0]);
578				} catch (MalformedURLException e1) {
579					params.url = null;
580				}
581				try {
582					params.size = Long.parseLong(parts[1]);
583				} catch (NumberFormatException e) {
584					params.size = 0;
585				}
586				try {
587					params.width = Integer.parseInt(parts[2]);
588				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
589					params.width = 0;
590				}
591				try {
592					params.height = Integer.parseInt(parts[3]);
593				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
594					params.height = 0;
595				}
596				break;
597			case 3:
598				try {
599					params.size = Long.parseLong(parts[0]);
600				} catch (NumberFormatException e) {
601					params.size = 0;
602				}
603				try {
604					params.width = Integer.parseInt(parts[1]);
605				} catch (NumberFormatException e) {
606					params.width = 0;
607				}
608				try {
609					params.height = Integer.parseInt(parts[2]);
610				} catch (NumberFormatException e) {
611					params.height = 0;
612				}
613				break;
614		}
615		return params;
616	}
617
618	public FileParams getLegacyFileParams() {
619		FileParams params = new FileParams();
620		if (body == null) {
621			return params;
622		}
623		String parts[] = body.split(",");
624		if (parts.length == 3) {
625			try {
626				params.size = Long.parseLong(parts[0]);
627			} catch (NumberFormatException e) {
628				return null;
629			}
630			try {
631				params.width = Integer.parseInt(parts[1]);
632			} catch (NumberFormatException e) {
633				return null;
634			}
635			try {
636				params.height = Integer.parseInt(parts[2]);
637			} catch (NumberFormatException e) {
638				return null;
639			}
640			return params;
641		} else {
642			return null;
643		}
644	}
645
646	public void untie() {
647		this.mNextMessage = null;
648		this.mPreviousMessage = null;
649	}
650
651	public boolean isFileOrImage() {
652		return type == TYPE_FILE || type == TYPE_IMAGE;
653	}
654
655	public boolean hasFileOnRemoteHost() {
656		return isFileOrImage() && getFileParams().url != null;
657	}
658
659	public boolean needsUploading() {
660		return isFileOrImage() && getFileParams().url == null;
661	}
662
663	public class FileParams {
664		public URL url;
665		public long size = 0;
666		public int width = 0;
667		public int height = 0;
668	}
669
670	public boolean isTrusted() {
671		return this.axolotlSession != null && this.axolotlSession.isTrusted();
672	}
673}