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