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		try {
434			URL url = new URL(this.getBody());
435			if (!url.getProtocol().equalsIgnoreCase("http")
436					&& !url.getProtocol().equalsIgnoreCase("https")) {
437				return false;
438					}
439			if (url.getPath() == null) {
440				return false;
441			}
442			String[] pathParts = url.getPath().split("/");
443			String filename;
444			if (pathParts.length > 0) {
445				filename = pathParts[pathParts.length - 1].toLowerCase();
446			} else {
447				return false;
448			}
449			String[] extensionParts = filename.split("\\.");
450			if (extensionParts.length == 2
451					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
452						extensionParts[extensionParts.length - 1])) {
453				return true;
454			} else if (extensionParts.length == 3
455					&& Arrays
456					.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
457					.contains(extensionParts[extensionParts.length - 1])
458					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
459						extensionParts[extensionParts.length - 2])) {
460				return true;
461			} else {
462				return false;
463			}
464		} catch (MalformedURLException e) {
465			return false;
466		}
467	}
468
469	public ImageParams getImageParams() {
470		ImageParams params = getLegacyImageParams();
471		if (params != null) {
472			return params;
473		}
474		params = new ImageParams();
475		if (this.downloadable != null) {
476			params.size = this.downloadable.getFileSize();
477		}
478		if (body == null) {
479			return params;
480		}
481		String parts[] = body.split("\\|");
482		if (parts.length == 1) {
483			try {
484				params.size = Long.parseLong(parts[0]);
485			} catch (NumberFormatException e) {
486				params.origin = parts[0];
487				try {
488					params.url = new URL(parts[0]);
489				} catch (MalformedURLException e1) {
490					params.url = null;
491				}
492			}
493		} else if (parts.length == 3) {
494			try {
495				params.size = Long.parseLong(parts[0]);
496			} catch (NumberFormatException e) {
497				params.size = 0;
498			}
499			try {
500				params.width = Integer.parseInt(parts[1]);
501			} catch (NumberFormatException e) {
502				params.width = 0;
503			}
504			try {
505				params.height = Integer.parseInt(parts[2]);
506			} catch (NumberFormatException e) {
507				params.height = 0;
508			}
509		} else if (parts.length == 4) {
510			params.origin = parts[0];
511			try {
512				params.url = new URL(parts[0]);
513			} catch (MalformedURLException e1) {
514				params.url = null;
515			}
516			try {
517				params.size = Long.parseLong(parts[1]);
518			} catch (NumberFormatException e) {
519				params.size = 0;
520			}
521			try {
522				params.width = Integer.parseInt(parts[2]);
523			} catch (NumberFormatException e) {
524				params.width = 0;
525			}
526			try {
527				params.height = Integer.parseInt(parts[3]);
528			} catch (NumberFormatException e) {
529				params.height = 0;
530			}
531		}
532		return params;
533	}
534
535	public ImageParams getLegacyImageParams() {
536		ImageParams params = new ImageParams();
537		if (body == null) {
538			return params;
539		}
540		String parts[] = body.split(",");
541		if (parts.length == 3) {
542			try {
543				params.size = Long.parseLong(parts[0]);
544			} catch (NumberFormatException e) {
545				return null;
546			}
547			try {
548				params.width = Integer.parseInt(parts[1]);
549			} catch (NumberFormatException e) {
550				return null;
551			}
552			try {
553				params.height = Integer.parseInt(parts[2]);
554			} catch (NumberFormatException e) {
555				return null;
556			}
557			return params;
558		} else {
559			return null;
560		}
561	}
562
563	public void untie() {
564		this.mNextMessage = null;
565		this.mPreviousMessage = null;
566	}
567
568	public boolean isFileOrImage() {
569		return type == TYPE_FILE || type == TYPE_IMAGE;
570	}
571
572	public class ImageParams {
573		public URL url;
574		public long size = 0;
575		public int width = 0;
576		public int height = 0;
577		public String origin;
578	}
579}