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