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