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