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