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