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