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 ImageParams getImageParams() {
489		ImageParams params = getLegacyImageParams();
490		if (params != null) {
491			return params;
492		}
493		params = new ImageParams();
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		if (parts.length == 1) {
502			try {
503				params.size = Long.parseLong(parts[0]);
504			} catch (NumberFormatException e) {
505				params.origin = parts[0];
506				try {
507					params.url = new URL(parts[0]);
508				} catch (MalformedURLException e1) {
509					params.url = null;
510				}
511			}
512		} else if (parts.length == 3) {
513			try {
514				params.size = Long.parseLong(parts[0]);
515			} catch (NumberFormatException e) {
516				params.size = 0;
517			}
518			try {
519				params.width = Integer.parseInt(parts[1]);
520			} catch (NumberFormatException e) {
521				params.width = 0;
522			}
523			try {
524				params.height = Integer.parseInt(parts[2]);
525			} catch (NumberFormatException e) {
526				params.height = 0;
527			}
528		} else if (parts.length == 4) {
529			params.origin = parts[0];
530			try {
531				params.url = new URL(parts[0]);
532			} catch (MalformedURLException e1) {
533				params.url = null;
534			}
535			try {
536				params.size = Long.parseLong(parts[1]);
537			} catch (NumberFormatException e) {
538				params.size = 0;
539			}
540			try {
541				params.width = Integer.parseInt(parts[2]);
542			} catch (NumberFormatException e) {
543				params.width = 0;
544			}
545			try {
546				params.height = Integer.parseInt(parts[3]);
547			} catch (NumberFormatException e) {
548				params.height = 0;
549			}
550		}
551		return params;
552	}
553
554	public ImageParams getLegacyImageParams() {
555		ImageParams params = new ImageParams();
556		if (body == null) {
557			return params;
558		}
559		String parts[] = body.split(",");
560		if (parts.length == 3) {
561			try {
562				params.size = Long.parseLong(parts[0]);
563			} catch (NumberFormatException e) {
564				return null;
565			}
566			try {
567				params.width = Integer.parseInt(parts[1]);
568			} catch (NumberFormatException e) {
569				return null;
570			}
571			try {
572				params.height = Integer.parseInt(parts[2]);
573			} catch (NumberFormatException e) {
574				return null;
575			}
576			return params;
577		} else {
578			return null;
579		}
580	}
581
582	public void untie() {
583		this.mNextMessage = null;
584		this.mPreviousMessage = null;
585	}
586
587	public boolean isFileOrImage() {
588		return type == TYPE_FILE || type == TYPE_IMAGE;
589	}
590
591	public boolean hasFileOnRemoteHost() {
592		return isFileOrImage() && getImageParams().url != null;
593	}
594
595	public boolean needsUploading() {
596		return isFileOrImage() && getImageParams().url == null;
597	}
598
599	public class ImageParams {
600		public URL url;
601		public long size = 0;
602		public int width = 0;
603		public int height = 0;
604		public String origin;
605	}
606}