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