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