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			 this.getEncryption() == message.getEncryption() &&
367			 this.getCounterpart() != null &&
368			 this.getCounterpart().equals(message.getCounterpart()) &&
369			 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
370			 !message.bodyContainsDownloadable() &&
371			 !this.bodyContainsDownloadable() &&
372			 !message.getBody().startsWith(ME_COMMAND) &&
373			 !this.getBody().startsWith(ME_COMMAND)
374			);
375	}
376
377	public String getMergedBody() {
378		final Message next = this.next();
379		if (this.mergeable(next)) {
380			return getBody().trim() + '\n' + next.getMergedBody();
381		}
382		return getBody().trim();
383	}
384
385	public boolean hasMeCommand() {
386		return getMergedBody().startsWith(ME_COMMAND);
387	}
388
389	public int getMergedStatus() {
390		return getStatus();
391	}
392
393	public long getMergedTimeSent() {
394		Message next = this.next();
395		if (this.mergeable(next)) {
396			return next.getMergedTimeSent();
397		} else {
398			return getTimeSent();
399		}
400	}
401
402	public boolean wasMergedIntoPrevious() {
403		Message prev = this.prev();
404		return prev != null && prev.mergeable(this);
405	}
406
407	public boolean trusted() {
408		Contact contact = this.getContact();
409		return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
410	}
411
412	public boolean bodyContainsDownloadable() {
413		try {
414			URL url = new URL(this.getBody());
415			if (!url.getProtocol().equalsIgnoreCase("http")
416					&& !url.getProtocol().equalsIgnoreCase("https")) {
417				return false;
418					}
419			if (url.getPath() == null) {
420				return false;
421			}
422			String[] pathParts = url.getPath().split("/");
423			String filename;
424			if (pathParts.length > 0) {
425				filename = pathParts[pathParts.length - 1].toLowerCase();
426			} else {
427				return false;
428			}
429			String[] extensionParts = filename.split("\\.");
430			if (extensionParts.length == 2
431					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
432						extensionParts[extensionParts.length - 1])) {
433				return true;
434			} else if (extensionParts.length == 3
435					&& Arrays
436					.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
437					.contains(extensionParts[extensionParts.length - 1])
438					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
439						extensionParts[extensionParts.length - 2])) {
440				return true;
441			} else {
442				return false;
443			}
444		} catch (MalformedURLException e) {
445			return false;
446		}
447	}
448
449	public ImageParams getImageParams() {
450		ImageParams params = getLegacyImageParams();
451		if (params != null) {
452			return params;
453		}
454		params = new ImageParams();
455		if (this.downloadable != null) {
456			params.size = this.downloadable.getFileSize();
457		}
458		if (body == null) {
459			return params;
460		}
461		String parts[] = body.split("\\|");
462		if (parts.length == 1) {
463			try {
464				params.size = Long.parseLong(parts[0]);
465			} catch (NumberFormatException e) {
466				params.origin = parts[0];
467				try {
468					params.url = new URL(parts[0]);
469				} catch (MalformedURLException e1) {
470					params.url = null;
471				}
472			}
473		} else if (parts.length == 3) {
474			try {
475				params.size = Long.parseLong(parts[0]);
476			} catch (NumberFormatException e) {
477				params.size = 0;
478			}
479			try {
480				params.width = Integer.parseInt(parts[1]);
481			} catch (NumberFormatException e) {
482				params.width = 0;
483			}
484			try {
485				params.height = Integer.parseInt(parts[2]);
486			} catch (NumberFormatException e) {
487				params.height = 0;
488			}
489		} else if (parts.length == 4) {
490			params.origin = parts[0];
491			try {
492				params.url = new URL(parts[0]);
493			} catch (MalformedURLException e1) {
494				params.url = null;
495			}
496			try {
497				params.size = Long.parseLong(parts[1]);
498			} catch (NumberFormatException e) {
499				params.size = 0;
500			}
501			try {
502				params.width = Integer.parseInt(parts[2]);
503			} catch (NumberFormatException e) {
504				params.width = 0;
505			}
506			try {
507				params.height = Integer.parseInt(parts[3]);
508			} catch (NumberFormatException e) {
509				params.height = 0;
510			}
511		}
512		return params;
513	}
514
515	public ImageParams getLegacyImageParams() {
516		ImageParams params = new ImageParams();
517		if (body == null) {
518			return params;
519		}
520		String parts[] = body.split(",");
521		if (parts.length == 3) {
522			try {
523				params.size = Long.parseLong(parts[0]);
524			} catch (NumberFormatException e) {
525				return null;
526			}
527			try {
528				params.width = Integer.parseInt(parts[1]);
529			} catch (NumberFormatException e) {
530				return null;
531			}
532			try {
533				params.height = Integer.parseInt(parts[2]);
534			} catch (NumberFormatException e) {
535				return null;
536			}
537			return params;
538		} else {
539			return null;
540		}
541	}
542
543	public void untie() {
544		this.mNextMessage = null;
545		this.mPreviousMessage = null;
546	}
547
548	public boolean isFileOrImage() {
549		return type == TYPE_FILE || type == TYPE_IMAGE;
550	}
551
552	public class ImageParams {
553		public URL url;
554		public long size = 0;
555		public int width = 0;
556		public int height = 0;
557		public String origin;
558	}
559}