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 = " \u200B\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.MESSAGE_MERGE_WINDOW * 1000;
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.treatAsDownloadable() == Decision.NO &&
379						this.treatAsDownloadable() == Decision.NO &&
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 enum Decision {
438		YES,
439		NO,
440		ASK
441	}
442
443	public Decision treatAsDownloadable() {
444		if (body.trim().contains(" ")) {
445			return Decision.NO;
446		}
447		try {
448			URL url = new URL(body);
449			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
450				return Decision.NO;
451			}
452			String path = url.getPath();
453			if (path == null || path.isEmpty()) {
454				return Decision.NO;
455			}
456
457			String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
458			String[] extensionParts = filename.split("\\.");
459			String extension;
460			String ref = url.getRef();
461			if (extensionParts.length == 2) {
462				extension = extensionParts[extensionParts.length - 1];
463			} else if (extensionParts.length == 3 && Arrays
464					.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
465					.contains(extensionParts[extensionParts.length - 1])) {
466				extension = extensionParts[extensionParts.length -2];
467			} else {
468				return Decision.NO;
469			}
470
471			if (Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(extension)) {
472				return Decision.YES;
473			} else if (ref != null && ref.matches("([A-Fa-f0-9]{2}){48}")) {
474				return Decision.ASK;
475			} else {
476				return Decision.NO;
477			}
478
479		} catch (MalformedURLException e) {
480			return Decision.NO;
481		}
482	}
483
484	public boolean bodyIsHeart() {
485		return body != null && UIHelper.HEARTS.contains(body.trim());
486	}
487
488	public FileParams getFileParams() {
489		FileParams params = getLegacyFileParams();
490		if (params != null) {
491			return params;
492		}
493		params = new FileParams();
494		if (this.downloadable != null) {
495			params.size = this.downloadable.getFileSize();
496		}
497		if (body == null) {
498			return params;
499		}
500		String parts[] = body.split("\\|");
501		switch (parts.length) {
502			case 1:
503				try {
504					params.size = Long.parseLong(parts[0]);
505				} catch (NumberFormatException e) {
506					try {
507						params.url = new URL(parts[0]);
508					} catch (MalformedURLException e1) {
509						params.url = null;
510					}
511				}
512				break;
513			case 2:
514			case 4:
515				try {
516					params.url = new URL(parts[0]);
517				} catch (MalformedURLException e1) {
518					params.url = null;
519				}
520				try {
521					params.size = Long.parseLong(parts[1]);
522				} catch (NumberFormatException e) {
523					params.size = 0;
524				}
525				try {
526					params.width = Integer.parseInt(parts[2]);
527				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
528					params.width = 0;
529				}
530				try {
531					params.height = Integer.parseInt(parts[3]);
532				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
533					params.height = 0;
534				}
535				break;
536			case 3:
537				try {
538					params.size = Long.parseLong(parts[0]);
539				} catch (NumberFormatException e) {
540					params.size = 0;
541				}
542				try {
543					params.width = Integer.parseInt(parts[1]);
544				} catch (NumberFormatException e) {
545					params.width = 0;
546				}
547				try {
548					params.height = Integer.parseInt(parts[2]);
549				} catch (NumberFormatException e) {
550					params.height = 0;
551				}
552				break;
553		}
554		return params;
555	}
556
557	public FileParams getLegacyFileParams() {
558		FileParams params = new FileParams();
559		if (body == null) {
560			return params;
561		}
562		String parts[] = body.split(",");
563		if (parts.length == 3) {
564			try {
565				params.size = Long.parseLong(parts[0]);
566			} catch (NumberFormatException e) {
567				return null;
568			}
569			try {
570				params.width = Integer.parseInt(parts[1]);
571			} catch (NumberFormatException e) {
572				return null;
573			}
574			try {
575				params.height = Integer.parseInt(parts[2]);
576			} catch (NumberFormatException e) {
577				return null;
578			}
579			return params;
580		} else {
581			return null;
582		}
583	}
584
585	public void untie() {
586		this.mNextMessage = null;
587		this.mPreviousMessage = null;
588	}
589
590	public boolean isFileOrImage() {
591		return type == TYPE_FILE || type == TYPE_IMAGE;
592	}
593
594	public boolean hasFileOnRemoteHost() {
595		return isFileOrImage() && getFileParams().url != null;
596	}
597
598	public boolean needsUploading() {
599		return isFileOrImage() && getFileParams().url == null;
600	}
601
602	public class FileParams {
603		public URL url;
604		public long size = 0;
605		public int width = 0;
606		public int height = 0;
607	}
608}