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