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 Transferable transferable = 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 Transferable getTransferable() {
312		return this.transferable;
313	}
314
315	public void setTransferable(Transferable transferable) {
316		this.transferable = transferable;
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.getTransferable() == null &&
368						message.getTransferable() == 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 boolean fixCounterpart() {
439		Presences presences = conversation.getContact().getPresences();
440		if (counterpart != null && presences.has(counterpart.getResourcepart())) {
441			return true;
442		} else if (presences.size() >= 1) {
443			try {
444				counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
445						conversation.getJid().getDomainpart(),
446						presences.asStringArray()[0]);
447				return true;
448			} catch (InvalidJidException e) {
449				counterpart = null;
450				return false;
451			}
452		} else {
453			counterpart = null;
454			return false;
455		}
456	}
457
458	public enum Decision {
459		MUST,
460		SHOULD,
461		NEVER,
462	}
463
464	private static String extractRelevantExtension(URL url) {
465		String path = url.getPath();
466		if (path == null || path.isEmpty()) {
467			return null;
468		}
469		String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
470		String[] extensionParts = filename.split("\\.");
471		if (extensionParts.length == 2) {
472			return extensionParts[extensionParts.length - 1];
473		} else if (extensionParts.length == 3 && Arrays
474				.asList(Transferable.VALID_CRYPTO_EXTENSIONS)
475				.contains(extensionParts[extensionParts.length - 1])) {
476			return extensionParts[extensionParts.length -2];
477		}
478		return null;
479	}
480
481	public String getMimeType() {
482		if (relativeFilePath != null) {
483			int start = relativeFilePath.lastIndexOf('.') + 1;
484			if (start < relativeFilePath.length()) {
485				return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
486			} else {
487				return null;
488			}
489		} else {
490			try {
491				return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
492			} catch (MalformedURLException e) {
493				return null;
494			}
495		}
496	}
497
498	public Decision treatAsDownloadable() {
499		if (body.trim().contains(" ")) {
500			return Decision.NEVER;
501		}
502		try {
503			URL url = new URL(body);
504			if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
505				return Decision.NEVER;
506			}
507			String extension = extractRelevantExtension(url);
508			if (extension == null) {
509				return Decision.NEVER;
510			}
511			String ref = url.getRef();
512			boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
513
514			if (encrypted) {
515				if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
516					return Decision.MUST;
517				} else {
518					return Decision.NEVER;
519				}
520			} else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)
521					|| Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
522				return Decision.SHOULD;
523			} else {
524				return Decision.NEVER;
525			}
526
527		} catch (MalformedURLException e) {
528			return Decision.NEVER;
529		}
530	}
531
532	public boolean bodyIsHeart() {
533		return body != null && UIHelper.HEARTS.contains(body.trim());
534	}
535
536	public FileParams getFileParams() {
537		FileParams params = getLegacyFileParams();
538		if (params != null) {
539			return params;
540		}
541		params = new FileParams();
542		if (this.transferable != null) {
543			params.size = this.transferable.getFileSize();
544		}
545		if (body == null) {
546			return params;
547		}
548		String parts[] = body.split("\\|");
549		switch (parts.length) {
550			case 1:
551				try {
552					params.size = Long.parseLong(parts[0]);
553				} catch (NumberFormatException e) {
554					try {
555						params.url = new URL(parts[0]);
556					} catch (MalformedURLException e1) {
557						params.url = null;
558					}
559				}
560				break;
561			case 2:
562			case 4:
563				try {
564					params.url = new URL(parts[0]);
565				} catch (MalformedURLException e1) {
566					params.url = null;
567				}
568				try {
569					params.size = Long.parseLong(parts[1]);
570				} catch (NumberFormatException e) {
571					params.size = 0;
572				}
573				try {
574					params.width = Integer.parseInt(parts[2]);
575				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
576					params.width = 0;
577				}
578				try {
579					params.height = Integer.parseInt(parts[3]);
580				} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
581					params.height = 0;
582				}
583				break;
584			case 3:
585				try {
586					params.size = Long.parseLong(parts[0]);
587				} catch (NumberFormatException e) {
588					params.size = 0;
589				}
590				try {
591					params.width = Integer.parseInt(parts[1]);
592				} catch (NumberFormatException e) {
593					params.width = 0;
594				}
595				try {
596					params.height = Integer.parseInt(parts[2]);
597				} catch (NumberFormatException e) {
598					params.height = 0;
599				}
600				break;
601		}
602		return params;
603	}
604
605	public FileParams getLegacyFileParams() {
606		FileParams params = new FileParams();
607		if (body == null) {
608			return params;
609		}
610		String parts[] = body.split(",");
611		if (parts.length == 3) {
612			try {
613				params.size = Long.parseLong(parts[0]);
614			} catch (NumberFormatException e) {
615				return null;
616			}
617			try {
618				params.width = Integer.parseInt(parts[1]);
619			} catch (NumberFormatException e) {
620				return null;
621			}
622			try {
623				params.height = Integer.parseInt(parts[2]);
624			} catch (NumberFormatException e) {
625				return null;
626			}
627			return params;
628		} else {
629			return null;
630		}
631	}
632
633	public void untie() {
634		this.mNextMessage = null;
635		this.mPreviousMessage = null;
636	}
637
638	public boolean isFileOrImage() {
639		return type == TYPE_FILE || type == TYPE_IMAGE;
640	}
641
642	public boolean hasFileOnRemoteHost() {
643		return isFileOrImage() && getFileParams().url != null;
644	}
645
646	public boolean needsUploading() {
647		return isFileOrImage() && getFileParams().url == null;
648	}
649
650	public class FileParams {
651		public URL url;
652		public long size = 0;
653		public int width = 0;
654		public int height = 0;
655	}
656}