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