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