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